diff --git a/lib/e2e/transaction.spec.ts b/lib/e2e/transaction.spec.ts index 36219ebc..c65f3972 100644 --- a/lib/e2e/transaction.spec.ts +++ b/lib/e2e/transaction.spec.ts @@ -1,4 +1,3 @@ -// @ts-nocheck /* eslint-disable */ import 'mocha'; import Big from 'big.js'; diff --git a/lib/src/core/cro.ts b/lib/src/core/cro.ts index e5ca6fdf..09efe91a 100644 --- a/lib/src/core/cro.ts +++ b/lib/src/core/cro.ts @@ -30,6 +30,10 @@ import { msgEditNFT } from '../transaction/msg/nft/MsgEditNFT'; import { msgTransferNFT } from '../transaction/msg/nft/MsgTransferNFT'; import { msgBurnNFT } from '../transaction/msg/nft/MsgBurnNFT'; import { msgSendV2 } from '../transaction/msg/v2/bank/v2.msgsend'; +import { msgFundCommunityPoolV2 } from '../transaction/msg/v2/distribution/v2.MsgFundCommunityPool'; +import { msgDepositV2 } from '../transaction/msg/v2/gov/v2.MsgDeposit'; +import { communityPoolSpendProposalV2 } from '../transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal'; +import { msgSubmitProposalV2 } from '../transaction/msg/v2/gov/v2.MsgSubmitProposal'; export const CroSDK = function (configs: InitConfigurations) { ow(configs, 'configs', owCroSDKInitParams); @@ -78,6 +82,16 @@ export const CroSDK = function (configs: InitConfigurations) { bank: { MsgSendV2: msgSendV2(configs), }, + distribution: { + MsgFundCommunityPoolV2: msgFundCommunityPoolV2(configs), + }, + gov: { + MsgDepositV2: msgDepositV2(configs), + MsgSubmitProposalV2: msgSubmitProposalV2(configs), + proposal: { + CommunityPoolSpendProposalV2: communityPoolSpendProposalV2(configs), + }, + }, }, Options: configs, }; diff --git a/lib/src/transaction/msg/bank/msgsend.ts b/lib/src/transaction/msg/bank/msgsend.ts index b64a1568..1c2bf8ba 100644 --- a/lib/src/transaction/msg/bank/msgsend.ts +++ b/lib/src/transaction/msg/bank/msgsend.ts @@ -65,7 +65,6 @@ export const msgSend = function (config: InitConfigurations) { return new MsgSend({ fromAddress: parsedMsg.from_address, toAddress: parsedMsg.to_address, - // TODO: Handle the complete list amount: cro.Coin.fromCustomAmountDenom(parsedMsg.amount[0].amount, parsedMsg.amount[0].denom), }); } @@ -124,6 +123,5 @@ export const msgSend = function (config: InitConfigurations) { export type MsgSendOptions = { fromAddress: string; toAddress: string; - // Todo: It should be ICoin[] amount: ICoin; }; diff --git a/lib/src/transaction/msg/distribution/MsgFundCommunityPool.spec.ts b/lib/src/transaction/msg/distribution/MsgFundCommunityPool.spec.ts index ace4e05e..2cfdda4d 100644 --- a/lib/src/transaction/msg/distribution/MsgFundCommunityPool.spec.ts +++ b/lib/src/transaction/msg/distribution/MsgFundCommunityPool.spec.ts @@ -136,4 +136,4 @@ describe('Testing MsgFundCommunityPool', function () { expect(msgFundCommPool.amount.toCosmosCoin().denom).to.eql('basetcro'); }); }); -}); +}); \ No newline at end of file diff --git a/lib/src/transaction/msg/distribution/MsgFundCommunityPool.ts b/lib/src/transaction/msg/distribution/MsgFundCommunityPool.ts index 965f4e28..c29fc0a4 100644 --- a/lib/src/transaction/msg/distribution/MsgFundCommunityPool.ts +++ b/lib/src/transaction/msg/distribution/MsgFundCommunityPool.ts @@ -76,7 +76,6 @@ export const msgFundCommunityPool = function (config: InitConfigurations) { return new MsgFundCommunityPool({ depositor: parsedMsg.depositor, - // TOdo: Handle the complete list amount: cro.Coin.fromCustomAmountDenom(parsedMsg.amount[0].amount, parsedMsg.amount[0].denom), }); } @@ -97,7 +96,6 @@ export const msgFundCommunityPool = function (config: InitConfigurations) { export type MsgFundCommunityPoolOptions = { depositor: string; - // Todo: Make it a list instead amount: ICoin; }; interface MsgFundCommunityPoolRaw { diff --git a/lib/src/transaction/msg/gov/proposal/SoftwareUpgradeProposal.ts b/lib/src/transaction/msg/gov/proposal/SoftwareUpgradeProposal.ts index 705f8e7a..be46ce38 100644 --- a/lib/src/transaction/msg/gov/proposal/SoftwareUpgradeProposal.ts +++ b/lib/src/transaction/msg/gov/proposal/SoftwareUpgradeProposal.ts @@ -6,7 +6,6 @@ import { IMsgProposalContent } from '../IMsgProposalContent'; import { owSoftwareUpgradeProposalOptions } from '../ow.types'; import { COSMOS_MSG_TYPEURL } from '../../../common/constants/typeurl'; import { Network } from '../../../../network/network'; -// import { Network } from '../../../../network/network'; export const softwareUpgradeProposal = function () { return class SoftwareUpgradeProposal implements IMsgProposalContent { diff --git a/lib/src/transaction/msg/ow.types.ts b/lib/src/transaction/msg/ow.types.ts index 78a0f8c0..2acba2a2 100644 --- a/lib/src/transaction/msg/ow.types.ts +++ b/lib/src/transaction/msg/ow.types.ts @@ -9,6 +9,13 @@ const voteOptionValidator = (val: number) => ({ message: (label: string) => `Expected ${label} to be one of the Vote options, got \`${val}\``, }); +const proposalContentValidatorFn = (val: object) => ({ + validator: isMsgProposalContent(val), + message: (label: string) => `Expected ${label} to be an instance of \`IMsgProposalContent\`, got \`${val}\``, +}); + +const owContent = () => owStrictObject().validate(proposalContentValidatorFn); + export const owVoteOption = () => ow.number.validate(voteOptionValidator); export const owMsgSendOptions = owStrictObject().exactShape({ @@ -22,15 +29,28 @@ export const v2 = { toAddress: ow.string, amount: ow.array.ofType(owCoin()), }), + owMsgFundCommunityPoolOptions: owStrictObject().exactShape({ + depositor: ow.string, + amount: ow.array.ofType(owCoin()), + }), + owMsgDepositOptions: owStrictObject().exactShape({ + depositor: ow.string, + proposalId: owBig(), + amount: ow.array.ofType(owCoin()), + }), + owCommunityPoolSpendProposalOptions: owStrictObject().exactShape({ + title: ow.string, + description: ow.string, + recipient: ow.string, + amount: ow.array.ofType(owCoin()), + }), + owMsgSubmitProposalOptions: owStrictObject().exactShape({ + proposer: ow.string, + initialDeposit: ow.array.ofType(owCoin()), + content: owContent(), + }), }; -const proposalContentValidatorFn = (val: object) => ({ - validator: isMsgProposalContent(val), - message: (label: string) => `Expected ${label} to be an instance of \`IMsgProposalContent\`, got \`${val}\``, -}); - -const owContent = () => owStrictObject().validate(proposalContentValidatorFn); - export const owMsgSubmitProposalOptions = owStrictObject().exactShape({ proposer: ow.string, initialDeposit: owCoin(), diff --git a/lib/src/transaction/msg/staking/MsgBeginRedelegate.ts b/lib/src/transaction/msg/staking/MsgBeginRedelegate.ts index 5799e50a..0c55517e 100644 --- a/lib/src/transaction/msg/staking/MsgBeginRedelegate.ts +++ b/lib/src/transaction/msg/staking/MsgBeginRedelegate.ts @@ -72,7 +72,6 @@ export const msgBeginRedelegate = function (config: InitConfigurations) { delegatorAddress: parsedMsg.delegator_address, validatorDstAddress: parsedMsg.validator_dst_address, validatorSrcAddress: parsedMsg.validator_src_address, - // TODO: Handle the complete list amount: cro.Coin.fromCustomAmountDenom(parsedMsg.amount.amount, parsedMsg.amount.denom), }); } diff --git a/lib/src/transaction/msg/v2/distribution/v2.MsgFundCommunityPool.spec.ts b/lib/src/transaction/msg/v2/distribution/v2.MsgFundCommunityPool.spec.ts new file mode 100644 index 00000000..f30c22ec --- /dev/null +++ b/lib/src/transaction/msg/v2/distribution/v2.MsgFundCommunityPool.spec.ts @@ -0,0 +1,139 @@ +/* eslint-disable */ +import { expect } from 'chai'; +import Big from 'big.js'; +import { fuzzyDescribe } from '../../../../test/mocha-fuzzy/suite'; +import { CroSDK, CroNetwork } from '../../../../core/cro'; +import { Secp256k1KeyPair } from '../../../../keypair/secp256k1'; +import { Bytes } from '../../../../utils/bytes/bytes'; +import * as legacyAmino from '../../../../cosmos/amino'; +import { Units } from '../../../../coin/coin'; + +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: '', + }, +}); +let amount = new cro.Coin('1000', Units.BASE) + +describe('Testing MsgFundCommunityPool', function () { + fuzzyDescribe('should throw Error when options is invalid', function (fuzzy) { + const anyValidOptions = { + depositor: 'tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3', + amount: [amount], + }; + const testRunner = fuzzy(fuzzy.ObjArg(anyValidOptions)); + + testRunner(function (options) { + if (options.valid) { + return; + } + expect(() => new cro.v2.distribution.MsgFundCommunityPoolV2(options.value)).to.throw( + 'Expected `communityPoolOptions` to be of type `object`', + ); + }); + }); + + it('Test appending MsgFundCommunityPool and signing it', function () { + const anyKeyPair = Secp256k1KeyPair.fromPrivKey( + Bytes.fromHexString('66633d18513bec30dd11a209f1ceb1787aa9e2069d5d47e590174dc9665102b3'), + ); + + const MsgFundCommunityPool = new cro.v2.distribution.MsgFundCommunityPoolV2({ + depositor: 'tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3', + amount: [amount], + }); + + const anySigner = { + publicKey: anyKeyPair.getPubKey(), + accountNumber: new Big(0), + accountSequence: new Big(10), + }; + + const rawTx = new cro.RawTransaction(); + + const signableTx = rawTx.appendMessage(MsgFundCommunityPool).addSigner(anySigner).toSignable(); + + const signedTx = signableTx.setSignature(0, anyKeyPair.sign(signableTx.toSignDocumentHash(0))).toSigned(); + + const signedTxHex = signedTx.encode().toHexString(); + expect(signedTxHex).to.be.eql( + '0a760a740a312f636f736d6f732e646973747269627574696f6e2e763162657461312e4d736746756e64436f6d6d756e697479506f6f6c123f0a100a08626173657463726f120431303030122b7463726f313635747a63726832796c3833673871657178756567326735677a6775353779336665336b633312580a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2103fd0d560b6c4aa1ca16721d039a192867c3457e19dad553edb98e7ba88b159c2712040a020801180a120410c09a0c1a400c93f8e74991dde45638e3d4366f7607fd9711272d0602f1e2e797d9180d25c30031955522e7ddc380d7ac4435e7f6d01fbea5db685655ba0b04d43b4135bf3d', + ); + }); + + describe('Testing MsgFundCommunityPool json', function () { + it('Test MsgFundCommunityPool conversion for amino json', function () { + const MsgWithdrawDelegatatorReward = new cro.v2.distribution.MsgFundCommunityPoolV2({ + depositor: 'tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3', + amount: [amount], + }); + + const rawMsg: legacyAmino.Msg = { + type: 'cosmos-sdk/MsgFundCommunityPool', + value: { + depositor: 'tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3', + amount: amount.toCosmosCoins(), + }, + }; + + expect(MsgWithdrawDelegatatorReward.toRawAminoMsg()).to.eqls(rawMsg); + }); + }); + + describe('Testing throw scenarios', function () { + it('Should throw on invalid depositor', function () { + expect(() => { + new cro.v2.distribution.MsgFundCommunityPoolV2({ + depositor: 'cro1xh3dqgljnydpwelzqf265edryrqrq7wzacx2nr', + amount: [amount], + }); + }).to.throw('Provided `depositor` address doesnt match network selected'); + }); + }); + describe('fromCosmosJSON', function () { + it('should throw Error if the JSON is not a MsgFundCommunityPool', function () { + const json = + '{ "@type": "/cosmos.bank.v1beta1.MsgCreateValidator", "amount": [{ "denom": "basetcro", "amount": "3478499933290496" }], "from_address": "tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg", "to_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3" }'; + expect(() => cro.v2.distribution.MsgFundCommunityPoolV2.fromCosmosMsgJSON(json, CroNetwork.Testnet)).to.throw( + 'Expected /cosmos.distribution.v1beta1.MsgFundCommunityPool but got /cosmos.bank.v1beta1.MsgCreateValidator', + ); + }); + + it('should throw Error when the `depositor` field is missing', function () { + const json = + '{"@type":"/cosmos.distribution.v1beta1.MsgFundCommunityPool","amount":[{ "denom": "basetcro", "amount": "3478499933290496" }]}'; + expect(() => cro.v2.distribution.MsgFundCommunityPoolV2.fromCosmosMsgJSON(json, CroNetwork.Testnet)).to.throw( + 'Expected property `depositor` to be of type `string` but received type `undefined` in object `communityPoolOptions`', + ); + }); + it('should throw Error when the amount field is missing', function () { + const json = + '{"@type":"/cosmos.distribution.v1beta1.MsgFundCommunityPool","depositor":"tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3"}'; + expect(() => cro.v2.distribution.MsgFundCommunityPoolV2.fromCosmosMsgJSON(json, CroNetwork.Testnet)).to.throw( + 'Invalid amount in the Msg.', + ); + }); + it('should return the `MsgFundCommunityPool` corresponding to the JSON', function () { + const json = + '{"@type":"/cosmos.distribution.v1beta1.MsgFundCommunityPool","amount":[{ "denom": "basetcro", "amount": "3478499933290496" }],"depositor":"tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3"}'; + + const msgFundCommPool = cro.v2.distribution.MsgFundCommunityPoolV2.fromCosmosMsgJSON(json, CroNetwork.Testnet); + expect(msgFundCommPool.depositor).to.eql('tcro165tzcrh2yl83g8qeqxueg2g5gzgu57y3fe3kc3'); + expect(msgFundCommPool.amount[0].toCosmosCoin().amount).to.eql('3478499933290496'); + expect(msgFundCommPool.amount[0].toCosmosCoin().denom).to.eql('basetcro'); + }); + }); +}); diff --git a/lib/src/transaction/msg/v2/distribution/v2.MsgFundCommunityPool.ts b/lib/src/transaction/msg/v2/distribution/v2.MsgFundCommunityPool.ts new file mode 100644 index 00000000..afc8a6a2 --- /dev/null +++ b/lib/src/transaction/msg/v2/distribution/v2.MsgFundCommunityPool.ts @@ -0,0 +1,110 @@ +import ow from 'ow'; +import { CosmosMsg } from '../../cosmosMsg'; +import { Msg } from '../../../../cosmos/v1beta1/types/msg'; +import { InitConfigurations, CroSDK } from '../../../../core/cro'; +import { AddressType, validateAddress } from '../../../../utils/address'; +import { v2 } from '../../ow.types'; +import { COSMOS_MSG_TYPEURL } from '../../../common/constants/typeurl'; +import * as legacyAmino from '../../../../cosmos/amino'; +import { ICoin } from '../../../../coin/coin'; +import { Network } from '../../../../network/network'; + +export const msgFundCommunityPoolV2 = function (config: InitConfigurations) { + return class MsgFundCommunityPoolV2 implements CosmosMsg { + // Normal user addresses with (t)cro prefix + public readonly depositor: string; + + public amount: ICoin[]; + + /** + * Constructor to create a new MsgFundCommunityPool + * @param {MsgFundCommunityPoolOptions} options + * @returns {MsgFundCommunityPoolV2} + * @throws {Error} when options is invalid + */ + constructor(options: MsgFundCommunityPoolOptions) { + ow(options, 'communityPoolOptions', v2.owMsgFundCommunityPoolOptions); + + this.depositor = options.depositor; + this.amount = options.amount; + + this.validateAddresses(); + } + + // eslint-disable-next-line class-methods-use-this + toRawAminoMsg(): legacyAmino.Msg { + return { + type: 'cosmos-sdk/MsgFundCommunityPool', + value: { + depositor: this.depositor, + amount: this.amount.map((coin) => coin.toCosmosCoin()), + }, + } as legacyAmino.MsgFundCommunityPool; + } + + /** + * Returns the raw Msg representation of MsgFundCommunityPool + * @returns {Msg} + */ + toRawMsg(): Msg { + return { + typeUrl: COSMOS_MSG_TYPEURL.distribution.MsgFundCommunityPool, + value: { + depositor: this.depositor, + amount: this.amount.map((coin) => coin.toCosmosCoin()), + }, + }; + } + + /** + * Returns an instance of MsgFundCommunityPool + * @param {string} msgJsonStr + * @param {Network} network + * @returns {MsgFundCommunityPool} + */ + public static fromCosmosMsgJSON(msgJsonStr: string, network: Network): MsgFundCommunityPoolV2 { + const parsedMsg = JSON.parse(msgJsonStr) as MsgFundCommunityPoolRaw; + const cro = CroSDK({ network }); + if (parsedMsg['@type'] !== COSMOS_MSG_TYPEURL.distribution.MsgFundCommunityPool) { + throw new Error( + `Expected ${COSMOS_MSG_TYPEURL.distribution.MsgFundCommunityPool} but got ${parsedMsg['@type']}`, + ); + } + if (!parsedMsg.amount || parsedMsg.amount.length < 1) { + throw new Error('Invalid amount in the Msg.'); + } + + return new MsgFundCommunityPoolV2({ + depositor: parsedMsg.depositor, + amount: parsedMsg.amount.map((coin) => cro.Coin.fromCustomAmountDenom(coin.amount, coin.denom)), + }); + } + + validateAddresses() { + if ( + !validateAddress({ + address: this.depositor, + network: config.network, + type: AddressType.USER, + }) + ) { + throw new TypeError('Provided `depositor` address doesnt match network selected'); + } + } + }; +}; + +export type MsgFundCommunityPoolOptions = { + depositor: string; + amount: ICoin[]; +}; +interface MsgFundCommunityPoolRaw { + '@type': string; + amount: Amount[]; + depositor: string; +} + +interface Amount { + denom: string; + amount: string; +} diff --git a/lib/src/transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal.spec.ts b/lib/src/transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal.spec.ts new file mode 100644 index 00000000..fa132230 --- /dev/null +++ b/lib/src/transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal.spec.ts @@ -0,0 +1,124 @@ +import 'mocha'; +import { expect } from 'chai'; + +import Big from 'big.js'; +import { Network } from '../../../../../network/network'; +import { CroSDK, CroNetwork } from '../../../../../core/cro'; +import { fuzzyDescribe } from '../../../../../test/mocha-fuzzy/suite'; +import { Units } from '../../../../../coin/coin'; +import { HDKey } from '../../../../../hdkey/hdkey'; +import { Secp256k1KeyPair } from '../../../../../keypair/secp256k1'; + +const PystaportTestNet: Network = { + rpcUrl: '', + defaultNodeUrl: '', + chainId: 'chainmaind', + addressPrefix: 'tcro', + validatorAddressPrefix: 'tcrocncl', + validatorPubKeyPrefix: 'tcrocnclconspub', + coin: { + baseDenom: 'basetcro', + croDenom: 'tcro', + }, + bip44Path: { + coinType: 1, + account: 0, + }, +}; +const cro = CroSDK({ network: PystaportTestNet }); +const coin = cro.Coin.fromBaseUnit('10000'); + +describe('Testing CommunityPoolSpendProposalV2 and its content types', function () { + const anyContent = new cro.v2.gov.proposal.CommunityPoolSpendProposalV2({ + title: 'Make new cosmos version backward compatible with pre release', + description: 'Lorem Ipsum ...', + recipient: 'tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg', + amount: [coin], + }); + + fuzzyDescribe('should throw Error when CommunityPoolSpendProposalV2 options is invalid', function (fuzzy) { + const anyValidProposalSubmission = { + proposer: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + initialDeposit: new cro.Coin('1200', Units.BASE), + content: anyContent, + }; + const testRunner = fuzzy(fuzzy.ObjArg(anyValidProposalSubmission)); + + testRunner(function (options) { + if (options.valid) { + return; + } + expect(() => new cro.v2.gov.proposal.CommunityPoolSpendProposalV2(options.value)).to.throw( + 'Expected `options` to be of type `object`', + ); + }); + }); + + it('Test Signing CommunityPoolSpendProposalV2 Type', function () { + const hdKey = HDKey.fromMnemonic( + 'order envelope snack half demand merry help obscure slogan like universe pond gain between brass settle pig float torch drama liberty grace check luxury', + ); + + const privKey = hdKey.derivePrivKey("m/44'/1'/0'/0/0"); + const keyPair = Secp256k1KeyPair.fromPrivKey(privKey); + + const CommunityPoolSpendProposalV2Content = new cro.v2.gov.proposal.CommunityPoolSpendProposalV2({ + title: 'Text Proposal Title', + description: 'Lorem Ipsum ... Checking cancel software upgrade', + recipient: 'tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg', + amount: [coin], + }); + + const CommunityPoolSpendProposalV2ChangeParam = new cro.gov.MsgSubmitProposal({ + proposer: 'tcro14sh490wk79dltea4udk95k7mw40wmvf77p0l5a', + initialDeposit: coin, + content: CommunityPoolSpendProposalV2Content, + }); + + const anySigner = { + publicKey: keyPair.getPubKey(), + accountNumber: new Big(6), + accountSequence: new Big(0), + }; + + const rawTx = new cro.RawTransaction(); + + const signableTx = rawTx + .appendMessage(CommunityPoolSpendProposalV2ChangeParam) + .addSigner(anySigner) + .toSignable(); + + const signedTx = signableTx.setSignature(0, keyPair.sign(signableTx.toSignDocumentHash(0))).toSigned(); + + const signedTxHex = signedTx.getHexEncoded(); + expect(signedTx.getTxHash()).to.be.eq('B68228C66AC221329AD61AB8924327F9062F80B9230C4947CBD5105A63896F99'); + expect(signedTxHex).to.be.eql( + '0ab3020ab0020a252f636f736d6f732e676f762e763162657461312e4d73675375626d697450726f706f73616c1286020ac3010a372f636f736d6f732e646973747269627574696f6e2e763162657461312e436f6d6d756e697479506f6f6c5370656e6450726f706f73616c1287010a13546578742050726f706f73616c205469746c6512304c6f72656d20497073756d202e2e2e20436865636b696e672063616e63656c20736f66747761726520757067726164651a2b7463726f317830376b6b6b6570666a32686c3865746c63757168656a376a6a366d7971727034387934686722110a08626173657463726f1205313030303012110a08626173657463726f120531303030301a2b7463726f31347368343930776b3739646c7465613475646b39356b376d773430776d7666373770306c356112580a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a210280c5e37a2bc3e68cc7c4aac78eac8c769cf58ce269ecd4307427aa16c2ba05a412040a0208011800120410c09a0c1a403ad1334095809e6daa04a1a3583703fd7ef651a97e8518450fe9c893f26296e462cd6734306c4d374a1fabf2e0387bf987c4440010507789c57ef5ae320f659a', + ); + }); + describe('fromCosmosJSON', function () { + it('should throw Error if the JSON is not a CommunityPoolSpendProposalV2', function () { + const json = + '{ "@type": "/cosmos.bank.v1beta1.MsgCreateValidator", "amount": [{ "denom": "basetcro", "amount": "3478499933290496" }], "from_address": "tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg", "to_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3" }'; + expect(() => + cro.v2.gov.proposal.CommunityPoolSpendProposalV2.fromCosmosMsgJSON(json, CroNetwork.Testnet), + ).to.throw( + 'Expected /cosmos.distribution.v1beta1.CommunityPoolSpendProposal but got /cosmos.bank.v1beta1.MsgCreateValidator', + ); + }); + + it('should return the CommunityPoolSpendProposalV2 corresponding to the JSON', function () { + const json = + '{"@type":"/cosmos.distribution.v1beta1.CommunityPoolSpendProposal","title": "Text Proposal Title", "description": "Lorem Ipsum ... Checking text proposal","amount": [{ "denom": "basetcro", "amount": "3478499933290496" }], "recipient": "tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg"}'; + const CommunityPoolSpendProposalV2 = cro.v2.gov.proposal.CommunityPoolSpendProposalV2.fromCosmosMsgJSON( + json, + CroNetwork.Testnet, + ); + + expect(CommunityPoolSpendProposalV2.title).to.eql('Text Proposal Title'); + + expect(CommunityPoolSpendProposalV2.description).to.eql('Lorem Ipsum ... Checking text proposal'); + expect(CommunityPoolSpendProposalV2.recipient).to.eql('tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg'); + }); + }); +}); diff --git a/lib/src/transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal.ts b/lib/src/transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal.ts new file mode 100644 index 00000000..9eb3be58 --- /dev/null +++ b/lib/src/transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal.ts @@ -0,0 +1,108 @@ +import ow from 'ow'; +import { v2 } from '../../../ow.types'; +import { InitConfigurations, CroSDK } from '../../../../../core/cro'; +import { IMsgProposalContent } from '../../../gov/IMsgProposalContent'; +import { ICoin } from '../../../../../coin/coin'; +import { google, cosmos } from '../../../../../cosmos/v1beta1/codec'; +import { COSMOS_MSG_TYPEURL } from '../../../../common/constants/typeurl'; +import { Network } from '../../../../../network/network'; +import { validateAddress, AddressType } from '../../../../../utils/address'; +import { Amount } from '../../../bank/msgsend'; + +export const communityPoolSpendProposalV2 = function (config: InitConfigurations) { + return class CommunityPoolSpendProposalV2 implements IMsgProposalContent { + /** CommunityPoolSpendProposal title. */ + public title: string; + + /** CommunityPoolSpendProposal description. */ + public description: string; + + /** CommunityPoolSpendProposal recipient. */ + public recipient: string; + + /** CommunityPoolSpendProposal amount. */ + public amount: ICoin[]; + + constructor(options: CommunityPoolSpendProposalOptions) { + ow(options, 'options', v2.owCommunityPoolSpendProposalOptions); + + this.title = options.title; + this.description = options.description; + this.recipient = options.recipient; + this.amount = options.amount; + } + + /** + * Returns the proto encoding representation of CommunityPoolSpendProposal + * @returns {google.protobuf.Any} + */ + getEncoded(): google.protobuf.Any { + const communityPoolSpend = { + title: this.title, + description: this.description, + recipient: this.recipient, + amount: this.amount.map((coin) => coin.toCosmosCoin()), + }; + + const spendProposal = cosmos.distribution.v1beta1.CommunityPoolSpendProposal.create(communityPoolSpend); + + return google.protobuf.Any.create({ + type_url: COSMOS_MSG_TYPEURL.upgrade.CommunityPoolSpendProposal, + value: cosmos.distribution.v1beta1.CommunityPoolSpendProposal.encode(spendProposal).finish(), + }); + } + + /** + * Returns an instance of CommunityPoolSpendProposal + * @param {string} msgJsonStr + * @param {Network} network + * @returns {CommunityPoolSpendProposal} + */ + public static fromCosmosMsgJSON(msgJsonStr: string, network: Network): CommunityPoolSpendProposalV2 { + const parsedMsg = JSON.parse(msgJsonStr) as CommunityPoolSpendProposalRaw; + if (parsedMsg['@type'] !== COSMOS_MSG_TYPEURL.upgrade.CommunityPoolSpendProposal) { + throw new Error( + `Expected ${COSMOS_MSG_TYPEURL.upgrade.CommunityPoolSpendProposal} but got ${parsedMsg['@type']}`, + ); + } + if (!parsedMsg.amount || parsedMsg.amount.length < 1) { + throw new Error('Invalid amount in the Msg.'); + } + const cro = CroSDK({ network }); + + return new CommunityPoolSpendProposalV2({ + description: parsedMsg.description, + title: parsedMsg.title, + recipient: parsedMsg.recipient, + amount: parsedMsg.amount.map((coin) => cro.Coin.fromCustomAmountDenom(coin.amount, coin.denom)), + }); + } + + validate() { + if ( + !validateAddress({ + address: this.recipient, + network: config.network, + type: AddressType.USER, + }) + ) { + throw new TypeError('Provided `recipient` doesnt match network selected'); + } + } + }; +}; + +export type CommunityPoolSpendProposalOptions = { + title: string; + description: string; + recipient: string; + amount: ICoin[]; +}; + +export interface CommunityPoolSpendProposalRaw { + '@type': string; + title: string; + description: string; + recipient: string; + amount: Amount[]; +} diff --git a/lib/src/transaction/msg/v2/gov/v2.MsgDeposit.spec.ts b/lib/src/transaction/msg/v2/gov/v2.MsgDeposit.spec.ts new file mode 100644 index 00000000..87936cce --- /dev/null +++ b/lib/src/transaction/msg/v2/gov/v2.MsgDeposit.spec.ts @@ -0,0 +1,148 @@ +import 'mocha'; +import { expect } from 'chai'; +import Big from 'big.js'; +import Long from 'long'; + +import { fuzzyDescribe } from '../../../../test/mocha-fuzzy/suite'; +import { Units } from '../../../../coin/coin'; +import { CroSDK, CroNetwork } from '../../../../core/cro'; +import { Msg } from '../../../../cosmos/v1beta1/types/msg'; +import { Secp256k1KeyPair } from '../../../../keypair/secp256k1'; +import { HDKey } from '../../../../hdkey/hdkey'; + +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 MsgDeposit', function () { + fuzzyDescribe('should throw Error when options is invalid', function (fuzzy) { + const anyValidOptions = { + proposalId: Big(1244000), + depositor: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: new cro.Coin('1200', Units.BASE), + }; + const testRunner = fuzzy(fuzzy.ObjArg(anyValidOptions)); + + testRunner(function (options) { + if (options.valid) { + return; + } + expect(() => new cro.v2.gov.MsgDepositV2(options.value)).to.throw( + 'Expected `options` to be of type `object`', + ); + }); + }); + + it('Test MsgDeposit conversion', function () { + const coin = new cro.Coin('12000500', Units.BASE); + + const msgDeposit = new cro.v2.gov.MsgDepositV2({ + proposalId: Big(1244000), + depositor: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: [coin], + }); + + const rawMsg: Msg = { + typeUrl: '/cosmos.gov.v1beta1.MsgDeposit', + value: { + proposalId: Long.fromNumber(1244000, true), + depositor: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: [ + { + denom: 'basetcro', + amount: '12000500', + }, + ], + }, + }; + expect(msgDeposit.toRawMsg()).to.eqls(rawMsg); + }); + + it('Test appendTxBody MsgDeposit Tx signing', function () { + const hdKey = HDKey.fromMnemonic( + 'team school reopen cave banner pass autumn march immune album hockey region baby critic insect armor pigeon owner number velvet romance flight blame tone', + ); + + const privKey = hdKey.derivePrivKey("m/44'/1'/0'/0/0"); + const keyPair = Secp256k1KeyPair.fromPrivKey(privKey); + + const coin = new cro.Coin('12000500', Units.CRO); + + const msgDeposit = new cro.v2.gov.MsgDepositV2({ + proposalId: Big(1244000), + depositor: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + amount: [coin], + }); + + const anySigner = { + publicKey: keyPair.getPubKey(), + accountNumber: new Big(1250), + accountSequence: new Big(0), + }; + + const rawTx = new cro.RawTransaction(); + + const signableTx = rawTx.appendMessage(msgDeposit).addSigner(anySigner).toSignable(); + + const signedTx = signableTx.setSignature(0, keyPair.sign(signableTx.toSignDocumentHash(0))).toSigned(); + + const signedTxHex = signedTx.encode().toHexString(); + expect(signedTxHex).to.be.eql( + '0a730a710a1e2f636f736d6f732e676f762e763162657461312e4d73674465706f736974124f08e0f64b122b7463726f3138346c7461326c7379753437767779703265387a6d746361336b3579713835703663347670331a1c0a08626173657463726f12103132303030353030303030303030303012580a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21030bf28c5f92c336db4703791691fa650fee408690b0a22c5ee4afb7e2508d32a712040a0208011800120410c09a0c1a40ba8c80028a85015ac737ca56603bef0a82e0fbd83f701ccbba02a4f381e5ee4a3d83af13cd02f1e9c1e8b386995d8468c2db1db73952c30fac6114004fe269c0', + ); + }); + describe('fromCosmosJSON', function () { + it('should throw Error if the JSON is not a MsgDeposit', function () { + const json = + '{ "@type": "/cosmos.bank.v1beta1.MsgCreateValidator", "amount": [{ "denom": "basetcro", "amount": "3478499933290496" }], "from_address": "tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg", "to_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3" }'; + expect(() => cro.v2.gov.MsgDepositV2.fromCosmosMsgJSON(json, CroNetwork.Testnet)).to.throw( + 'Expected /cosmos.gov.v1beta1.MsgDeposit but got /cosmos.bank.v1beta1.MsgCreateValidator', + ); + }); + it('should throw Error when the `proposal_id` field is missing', function () { + const json = + '{"@type":"/cosmos.gov.v1beta1.MsgDeposit","depositor":"tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3","amount":[{"amount": "1234567890", "denom":"basetcro"}]}'; + expect(() => cro.v2.gov.MsgDepositV2.fromCosmosMsgJSON(json, CroNetwork.Testnet)).to.throw( + 'Invalid `proposal_id` in JSON.', + ); + }); + it('should throw Error when the `depositor` field is missing', function () { + const json = + '{"@type":"/cosmos.gov.v1beta1.MsgDeposit","proposal_id":"1244000","amount":[{"amount": "1234567890", "denom":"basetcro"}]}'; + expect(() => cro.v2.gov.MsgDepositV2.fromCosmosMsgJSON(json, CroNetwork.Testnet)).to.throw( + 'Expected property `depositor` 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.gov.v1beta1.MsgDeposit","proposal_id":"1244000","depositor":"tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3"}'; + expect(() => cro.v2.gov.MsgDepositV2.fromCosmosMsgJSON(json, CroNetwork.Testnet)).to.throw( + 'Invalid amount in the Msg.', + ); + }); + it('should return the MsgDeposit corresponding to the JSON', function () { + const json = + '{"@type":"/cosmos.gov.v1beta1.MsgDeposit","proposal_id":"1244000","depositor":"tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3","amount":[{"amount": "1234567890", "denom":"basetcro"}]}'; + const MsgDeposit = cro.v2.gov.MsgDepositV2.fromCosmosMsgJSON(json, CroNetwork.Testnet); + expect(MsgDeposit.depositor).to.eql('tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3'); + expect((MsgDeposit.proposalId as Big).toString()).to.eql('1244000'); + expect(MsgDeposit.amount[0].toCosmosCoin().amount).to.eql('1234567890'); + expect(MsgDeposit.amount[0].toCosmosCoin().denom).to.eql('basetcro'); + }); + }); +}); diff --git a/lib/src/transaction/msg/v2/gov/v2.MsgDeposit.ts b/lib/src/transaction/msg/v2/gov/v2.MsgDeposit.ts new file mode 100644 index 00000000..f4802e71 --- /dev/null +++ b/lib/src/transaction/msg/v2/gov/v2.MsgDeposit.ts @@ -0,0 +1,115 @@ +/* eslint-disable camelcase */ +import Big from 'big.js'; +import Long from 'long'; +import ow from 'ow'; +import { InitConfigurations, CroSDK } from '../../../../core/cro'; +import { CosmosMsg } from '../../cosmosMsg'; +import { Msg } from '../../../../cosmos/v1beta1/types/msg'; +import { ICoin } from '../../../../coin/coin'; +import { AddressType, validateAddress } from '../../../../utils/address'; +import { COSMOS_MSG_TYPEURL } from '../../../common/constants/typeurl'; +import { v2 } from '../../ow.types'; +import * as legacyAmino from '../../../../cosmos/amino'; +import { Amount } from '../../bank/msgsend'; +import { Network } from '../../../../network/network'; + +export const msgDepositV2 = function (config: InitConfigurations) { + return class MsgDepositV2 implements CosmosMsg { + public proposalId: Big; + + public depositor: string; + + public amount: ICoin[]; + + /** + * Constructor to create a new MsgDeposit + * @param {MsgDepositOptions} options + * @returns {MsgDeposit} + * @throws {Error} when options is invalid + */ + constructor(options: MsgDepositOptions) { + ow(options, 'options', v2.owMsgDepositOptions); + + this.proposalId = options.proposalId; + this.depositor = options.depositor; + this.amount = options.amount; + + this.validate(); + } + + // eslint-disable-next-line class-methods-use-this + toRawAminoMsg(): legacyAmino.Msg { + throw new Error('Method not implemented.'); + } + + /** + * Returns the raw Msg representation of MsgDeposit + * @returns {Msg} + */ + toRawMsg(): Msg { + const proposal = Long.fromNumber(this.proposalId.toNumber(), true); + return { + typeUrl: COSMOS_MSG_TYPEURL.MsgDeposit, + value: { + proposalId: proposal, + depositor: this.depositor, + amount: this.amount.map((coin) => coin.toCosmosCoin()), + }, + }; + } + + /** + * Returns an instance of MsgDeposit + * @param {string} msgJsonStr + * @param {Network} network + * @returns {MsgDeposit} + */ + public static fromCosmosMsgJSON(msgJsonStr: string, network: Network): MsgDepositV2 { + const parsedMsg = JSON.parse(msgJsonStr) as MsgDepositRaw; + if (parsedMsg['@type'] !== COSMOS_MSG_TYPEURL.MsgDeposit) { + throw new Error(`Expected ${COSMOS_MSG_TYPEURL.MsgDeposit} but got ${parsedMsg['@type']}`); + } + + if (!parsedMsg.proposal_id) { + throw new Error('Invalid `proposal_id` in JSON.'); + } + + if (!parsedMsg.amount || parsedMsg.amount.length < 1) { + throw new Error('Invalid amount in the Msg.'); + } + + const cro = CroSDK({ network }); + + return new MsgDepositV2({ + proposalId: new Big(parsedMsg.proposal_id), + depositor: parsedMsg.depositor, + amount: parsedMsg.amount.map((coin) => cro.Coin.fromCustomAmountDenom(coin.amount, coin.denom)), + }); + } + + validate() { + if ( + !validateAddress({ + address: this.depositor, + network: config.network, + type: AddressType.USER, + }) + ) { + throw new TypeError('Provided `depositor` doesnt match network selected'); + } + } + }; +}; + +export type MsgDepositOptions = { + proposalId: Big; + depositor: string; + amount: ICoin[]; +}; + +interface MsgDepositRaw { + '@type': string; + proposal_id: string; + depositor: string; + amount: Amount[]; +} diff --git a/lib/src/transaction/msg/v2/gov/v2.MsgSubmitProposal.spec.ts b/lib/src/transaction/msg/v2/gov/v2.MsgSubmitProposal.spec.ts new file mode 100644 index 00000000..ee02a093 --- /dev/null +++ b/lib/src/transaction/msg/v2/gov/v2.MsgSubmitProposal.spec.ts @@ -0,0 +1,209 @@ +import 'mocha'; +import { expect } from 'chai'; + +import Big from 'big.js'; +import { fuzzyDescribe } from '../../../../test/mocha-fuzzy/suite'; +import { Units } from '../../../../coin/coin'; +import { CroSDK, CroNetwork } from '../../../../core/cro'; +import { HDKey } from '../../../../hdkey/hdkey'; +import { Secp256k1KeyPair } from '../../../../keypair/secp256k1'; +import { Network } from '../../../../network/network'; + +const PystaportTestNet: Network = { + defaultNodeUrl: '', + chainId: 'chainmaind', + addressPrefix: 'tcro', + validatorAddressPrefix: 'tcrocncl', + validatorPubKeyPrefix: 'tcrocnclconspub', + coin: { + baseDenom: 'basetcro', + croDenom: 'tcro', + }, + bip44Path: { + coinType: 1, + account: 0, + }, + rpcUrl: '', +}; +const cro = CroSDK({ network: PystaportTestNet }); + +describe('Testing MsgSubmitProposalV2 and its content types', function () { + const anyContent = new cro.v2.gov.proposal.CommunityPoolSpendProposalV2({ + title: 'Make new cosmos version backward compatible with pre release', + description: 'Lorem Ipsum ... A great proposal to increate backward compatibility and initial work on IBC', + recipient: 'tcro1nhe3qasy0ayhje95mtsvppyg67d3zswf04sda8', + amount: [new cro.Coin('1200', Units.BASE)], + }); + + fuzzyDescribe('should throw Error when MsgSubmitProposal options is invalid', function (fuzzy) { + const anyValidProposalSubmission = { + proposer: 'tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3', + initialDeposit: [new cro.Coin('1200', Units.BASE)], + content: anyContent, + }; + const testRunner = fuzzy(fuzzy.ObjArg(anyValidProposalSubmission)); + + testRunner(function (options) { + if (options.valid) { + return; + } + expect(() => new cro.v2.gov.MsgSubmitProposalV2(options.value)).to.throw( + 'Expected `options` to be of type `object`', + ); + }); + }); + + fuzzyDescribe('should throw Error when CommunityPoolSpendProposal options is invalid', function (fuzzy) { + const anyValidCommunityPoolSpendProposal = { + title: 'Make new cosmos version backward compatible with pre release', + description: 'Lorem Ipsum ... A great proposal to ...', + recipient: 'tcro1nhe3qasy0ayhje95mtsvppyg67d3zswf04sda8', + amount: [new cro.Coin('1200', Units.BASE)], + }; + const testRunner = fuzzy(fuzzy.ObjArg(anyValidCommunityPoolSpendProposal)); + + testRunner(function (options) { + if (options.valid) { + return; + } + expect(() => new cro.v2.gov.proposal.CommunityPoolSpendProposalV2(options.value)).to.throw( + 'Expected `options` to be of type `object`', + ); + }); + }); + + fuzzyDescribe('should throw Error when ParamChangeProposal options is invalid', function (fuzzy) { + const anyValidParamChangeProposal = new cro.gov.proposal.ParamChangeProposal({ + title: 'Change a param to something more optimized', + description: 'Lorem Ipsum ... The param should be changed to something more optimized', + paramChanges: [ + { + subspace: 'staking', + key: 'MaxValidators', + value: '12', + }, + ], + }); + const testRunner = fuzzy(fuzzy.ObjArg(anyValidParamChangeProposal)); + + testRunner(function (options) { + if (options.valid) { + return; + } + expect(() => new cro.gov.proposal.ParamChangeProposal(options.value)).to.throw( + 'Expected `options` to be of type `object`', + ); + }); + }); + + it('Test Signing MsgSubmitProposal of CommunityPoolSpendProposal Type', function () { + const hdKey = HDKey.fromMnemonic( + 'guilt shield sting fluid wet east video business fold agree capital galaxy rapid almost melt piano taste guide spoil pull pigeon wood fit escape', + ); + + const privKey = hdKey.derivePrivKey("m/44'/1'/0'/0/0"); + const keyPair = Secp256k1KeyPair.fromPrivKey(privKey); + + const coin = new cro.Coin('120', Units.CRO); + + const communityPoolSpentContent = new cro.v2.gov.proposal.CommunityPoolSpendProposalV2({ + title: 'Make new cosmos version backward compatible with pre release', + description: 'Lorem Ipsum ... A great proposal to increate backward compatibility and initial work on IBC', + recipient: 'tcro1nhe3qasy0ayhje95mtsvppyg67d3zswf04sda8', + amount: [coin], + }); + + const msgSubmitProposalCommunitySpend = new cro.v2.gov.MsgSubmitProposalV2({ + proposer: 'tcro1nhe3qasy0ayhje95mtsvppyg67d3zswf04sda8', + initialDeposit: [coin], + content: communityPoolSpentContent, + }); + + const anySigner = { + publicKey: keyPair.getPubKey(), + accountNumber: new Big(6), + accountSequence: new Big(0), + }; + + const rawTx = new cro.RawTransaction(); + + const signableTx = rawTx.appendMessage(msgSubmitProposalCommunitySpend).addSigner(anySigner).toSignable(); + + const signedTx = signableTx.setSignature(0, keyPair.sign(signableTx.toSignDocumentHash(0))).toSigned(); + + const signedTxHex = signedTx.getHexEncoded(); + expect(signedTx.getTxHash()).to.be.eq('2A177BC5B770C503096F5AA2CCF0B89EC8FC2D33C135630A38922266E6EE1EF1'); + expect(signedTxHex).to.be.eql( + '0a93030a90030a252f636f736d6f732e676f762e763162657461312e4d73675375626d697450726f706f73616c12e6020a9d020a372f636f736d6f732e646973747269627574696f6e2e763162657461312e436f6d6d756e697479506f6f6c5370656e6450726f706f73616c12e1010a3c4d616b65206e657720636f736d6f732076657273696f6e206261636b7761726420636f6d70617469626c652077697468207072652072656c65617365125b4c6f72656d20497073756d202e2e2e20412067726561742070726f706f73616c20746f20696e637265617465206261636b7761726420636f6d7061746962696c69747920616e6420696e697469616c20776f726b206f6e204942431a2b7463726f316e68653371617379306179686a6539356d74737670707967363764337a73776630347364613822170a08626173657463726f120b313230303030303030303012170a08626173657463726f120b31323030303030303030301a2b7463726f316e68653371617379306179686a6539356d74737670707967363764337a73776630347364613812580a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a2102046b34d613be4ad7e79dcadf13fb4ce8d8d7ffeee7b554c32e924906d4e5664b12040a0208011800120410c09a0c1a4044e8f4777960420e4759bbe527ba18ad47bafff359d02c1eabb5fa14dd68a7c15373f5773810104d56a4fcb43033cb354b4c4c92c568be99ed1f06422f7d7d60', + ); + }); + + it('Test Signing MsgSubmitProposal of ParamChangeProposal Type', function () { + const hdKey = HDKey.fromMnemonic( + 'order envelope snack half demand merry help obscure slogan like universe pond gain between brass settle pig float torch drama liberty grace check luxury', + ); + + const privKey = hdKey.derivePrivKey("m/44'/1'/0'/0/0"); + const keyPair = Secp256k1KeyPair.fromPrivKey(privKey); + + const coin = new cro.Coin('120', Units.CRO); + + const communityPoolSpentContent = new cro.gov.proposal.ParamChangeProposal({ + title: 'Change a param to something more optimized', + description: 'Lorem Ipsum ... The param should be changed to something more optimized', + paramChanges: [ + { + subspace: 'staking', + key: 'MaxValidators', + value: '12', + }, + ], + }); + + const msgSubmitProposalChangeParam = new cro.v2.gov.MsgSubmitProposalV2({ + proposer: 'tcro14sh490wk79dltea4udk95k7mw40wmvf77p0l5a', + initialDeposit: [coin], + content: communityPoolSpentContent, + }); + + const anySigner = { + publicKey: keyPair.getPubKey(), + accountNumber: new Big(6), + accountSequence: new Big(0), + }; + + const rawTx = new cro.RawTransaction(); + + const signableTx = rawTx.appendMessage(msgSubmitProposalChangeParam).addSigner(anySigner).toSignable(); + + const signedTx = signableTx.setSignature(0, keyPair.sign(signableTx.toSignDocumentHash(0))).toSigned(); + + const signedTxHex = signedTx.getHexEncoded(); + expect(signedTx.getTxHash()).to.be.eq('AFEBA2DE9891AF22040359C8AACEF2836E8BF1276D66505DE36559F3E912EFF8'); + expect(signedTxHex).to.be.eql( + '0abc020ab9020a252f636f736d6f732e676f762e763162657461312e4d73675375626d697450726f706f73616c128f020ac6010a2e2f636f736d6f732e706172616d732e763162657461312e506172616d657465724368616e676550726f706f73616c1293010a2a4368616e6765206120706172616d20746f20736f6d657468696e67206d6f7265206f7074696d697a656412474c6f72656d20497073756d202e2e2e2054686520706172616d2073686f756c64206265206368616e67656420746f20736f6d657468696e67206d6f7265206f7074696d697a65641a1c0a077374616b696e67120d4d617856616c696461746f72731a02313212170a08626173657463726f120b31323030303030303030301a2b7463726f31347368343930776b3739646c7465613475646b39356b376d773430776d7666373770306c356112580a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a210280c5e37a2bc3e68cc7c4aac78eac8c769cf58ce269ecd4307427aa16c2ba05a412040a0208011800120410c09a0c1a4072bd47137d440036995ea6b5c4754b4f15609df2fdd17496d6c39f47d6663d0e51d171bcae92fc6078496cf657e2a705cd59b0d882cf0356463e57b26e285941', + ); + }); + + describe('fromCosmosJSON', function () { + it('should throw Error if the JSON is not a MsgSubmitProposal', function () { + const json = + '{ "@type": "/cosmos.bank.v1beta1.MsgCreateValidator", "amount": [{ "denom": "basetcro", "amount": "3478499933290496" }], "from_address": "tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg", "to_address": "tcro184lta2lsyu47vwyp2e8zmtca3k5yq85p6c4vp3" }'; + expect(() => cro.v2.gov.MsgSubmitProposalV2.fromCosmosMsgJSON(json, CroNetwork.Testnet)).to.throw( + 'Expected /cosmos.gov.v1beta1.MsgSubmitProposal but got /cosmos.bank.v1beta1.MsgCreateValidator', + ); + }); + + it('should return the MsgSubmitProposal corresponding to the JSON', function () { + const json = + '{"@type":"/cosmos.gov.v1beta1.MsgSubmitProposal","initial_deposit":[{"denom":"basetcro","amount":"12000000000"}],"content":{"@type":"/cosmos.params.v1beta1.ParameterChangeProposal","changes":[{"subspace":"staking","key":"MaxValidators","value":"12"}],"title":"Change a param to something more optimized","description":"Lorem Ipsum ... The param should be changed to something more optimized"},"proposer":"tcro14sh490wk79dltea4udk95k7mw40wmvf77p0l5a"}'; + const MsgDeposit = cro.v2.gov.MsgSubmitProposalV2.fromCosmosMsgJSON(json, CroNetwork.Testnet); + expect(MsgDeposit.initialDeposit[0].toCosmosCoin().amount).to.eql('12000000000'); + expect(MsgDeposit.initialDeposit[0].toCosmosCoin().denom).to.eql('basetcro'); + + expect(MsgDeposit.proposer).to.eql('tcro14sh490wk79dltea4udk95k7mw40wmvf77p0l5a'); + + expect(MsgDeposit.content.getEncoded().type_url).to.eql('/cosmos.params.v1beta1.ParameterChangeProposal'); + }); + }); +}); diff --git a/lib/src/transaction/msg/v2/gov/v2.MsgSubmitProposal.ts b/lib/src/transaction/msg/v2/gov/v2.MsgSubmitProposal.ts new file mode 100644 index 00000000..51ddf11b --- /dev/null +++ b/lib/src/transaction/msg/v2/gov/v2.MsgSubmitProposal.ts @@ -0,0 +1,122 @@ +/* eslint-disable camelcase */ +import ow from 'ow'; +import { IMsgProposalContent } from '../../gov/IMsgProposalContent'; +import { InitConfigurations, CroSDK } from '../../../../core/cro'; +import { CosmosMsg } from '../../cosmosMsg'; +import { ICoin } from '../../../../coin/coin'; +import { v2 } from '../../ow.types'; +import { Msg } from '../../../../cosmos/v1beta1/types/msg'; +import { COSMOS_MSG_TYPEURL, typeUrlToMsgClassMapping } from '../../../common/constants/typeurl'; +import { Network } from '../../../../network/network'; +import { validateAddress, AddressType } from '../../../../utils/address'; +import { Amount } from '../../bank/msgsend'; +import * as legacyAmino from '../../../../cosmos/amino'; + +export const msgSubmitProposalV2 = function (config: InitConfigurations) { + return class MsgSubmitProposalV2 implements CosmosMsg { + public readonly proposer: string; + + public readonly initialDeposit: ICoin[]; + + public readonly content: IMsgProposalContent; + + /** + * Constructor to create a new MsgSubmitProposal + * @param {ProposalOptions} options + * @returns {MsgSubmitProposal} + * @throws {Error} when options is invalid + */ + constructor(options: ProposalOptions) { + ow(options, 'options', v2.owMsgSubmitProposalOptions); + this.proposer = options.proposer; + this.initialDeposit = options.initialDeposit; + this.content = options.content; + + this.validate(); + } + + // eslint-disable-next-line class-methods-use-this + toRawAminoMsg(): legacyAmino.Msg { + throw new Error('Method not implemented.'); + } + + /** + * Returns the raw Msg representation of MsgSubmitProposal + * @returns {Msg} + */ + toRawMsg(): Msg { + return { + typeUrl: COSMOS_MSG_TYPEURL.MsgSubmitProposal, + value: { + proposer: this.proposer, + content: this.content.getEncoded(), + initialDeposit: this.initialDeposit.map((coin) => coin.toCosmosCoin()), + }, + }; + } + + /** + * Returns an instance of MsgSubmitProposal + * @param {string} msgJsonStr + * @param {Network} network + * @returns {MsgSubmitProposal} + */ + public static fromCosmosMsgJSON(msgJsonStr: string, network: Network): MsgSubmitProposalV2 { + const parsedMsg = JSON.parse(msgJsonStr) as MsgSubmitProposalRaw; + if (parsedMsg['@type'] !== COSMOS_MSG_TYPEURL.MsgSubmitProposal) { + throw new Error(`Expected ${COSMOS_MSG_TYPEURL.MsgSubmitProposal} but got ${parsedMsg['@type']}`); + } + + if (!parsedMsg.initial_deposit || parsedMsg.initial_deposit.length < 1) { + throw new Error('Invalid initial_deposit in the Msg.'); + } + + const cro = CroSDK({ network }); + + const jsonContentRaw = parsedMsg.content; + const contentClassInstance = typeUrlToMsgClassMapping(cro, jsonContentRaw['@type']); + const nativeContentMsg: IMsgProposalContent = contentClassInstance.fromCosmosMsgJSON( + JSON.stringify(jsonContentRaw), + network, + ); + + return new MsgSubmitProposalV2({ + proposer: parsedMsg.proposer, + initialDeposit: parsedMsg.initial_deposit.map((coin) => + cro.Coin.fromCustomAmountDenom(coin.amount, coin.denom), + ), + content: nativeContentMsg, + }); + } + + validate() { + if ( + !validateAddress({ + address: this.proposer, + network: config.network, + type: AddressType.USER, + }) + ) { + throw new TypeError('Provided `proposer` doesnt match network selected'); + } + } + }; +}; + +export type ProposalOptions = { + proposer: string; + initialDeposit: ICoin[]; + content: IMsgProposalContent; +}; + +export interface MsgSubmitProposalRaw { + '@type': string; + initial_deposit: Amount[]; + content: Content; + proposer: string; +} + +export interface Content { + '@type': string; + [key: string]: any; +} diff --git a/lib/src/utils/txDecoder.ts b/lib/src/utils/txDecoder.ts index 8d10fe68..2c311b98 100644 --- a/lib/src/utils/txDecoder.ts +++ b/lib/src/utils/txDecoder.ts @@ -129,9 +129,7 @@ function decodeAnyType(typeUrl: string, value: Uint8Array) { } function handleSpecialParams(decodedParams: any) { - // handle all MsgSubmitProposal - // TODO: Make it generic when encounter new cases - + // handle all `MsgSubmitProposal` related messages const clonedDecodedParams = { ...decodedParams }; if (decodedParams.content && Object.keys(decodedParams.content).length !== 0) { clonedDecodedParams.content = decodeAnyType(decodedParams.content.type_url, decodedParams.content.value); @@ -182,7 +180,7 @@ const handleCustomTypes = (obj: any) => { Object.keys(obj).forEach((k) => { if (typeof obj[k] === 'object' && obj[k] !== null) { if (obj[k] instanceof Long) { - // todo: I will fix the below unsuggested version + // Recursively keeping same object obj[k] = obj[k].toString(10); // eslint-disable-line no-param-reassign } handleCustomTypes(obj[k]);