From d23975b036c2fa1605e0678dc48f201c9af0c433 Mon Sep 17 00:00:00 2001 From: cdc-Hitesh Date: Tue, 29 Jun 2021 19:26:18 +0530 Subject: [PATCH] #281: Support MsgFundCommunitypool for V2 --- lib/src/core/cro.ts | 4 + .../distribution/MsgFundCommunityPool.spec.ts | 2 - lib/src/transaction/msg/ow.types.ts | 6 + .../v2.CommunityPoolSpendProposal.spec.ts | 121 ++++++++++++++++++ .../proposal/v2.CommunityPoolSpendProposal.ts | 108 ++++++++++++++++ 5 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 lib/src/transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal.spec.ts create mode 100644 lib/src/transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal.ts diff --git a/lib/src/core/cro.ts b/lib/src/core/cro.ts index 503fc660..88e39e76 100644 --- a/lib/src/core/cro.ts +++ b/lib/src/core/cro.ts @@ -32,6 +32,7 @@ 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'; export const CroSDK = function (configs: InitConfigurations) { ow(configs, 'configs', owCroSDKInitParams); @@ -85,6 +86,9 @@ export const CroSDK = function (configs: InitConfigurations) { }, gov: { MsgDepositV2: msgDepositV2(configs), + proposal: { + CommunityPoolSpendProposalV2: communityPoolSpendProposalV2(configs), + }, }, }, Options: configs, diff --git a/lib/src/transaction/msg/distribution/MsgFundCommunityPool.spec.ts b/lib/src/transaction/msg/distribution/MsgFundCommunityPool.spec.ts index 965f4e28..c29fc0a4 100644 --- a/lib/src/transaction/msg/distribution/MsgFundCommunityPool.spec.ts +++ b/lib/src/transaction/msg/distribution/MsgFundCommunityPool.spec.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/ow.types.ts b/lib/src/transaction/msg/ow.types.ts index 84b44fe0..738f27cd 100644 --- a/lib/src/transaction/msg/ow.types.ts +++ b/lib/src/transaction/msg/ow.types.ts @@ -31,6 +31,12 @@ export const v2 = { proposalId: owBig(), amount: ow.array.ofType(owCoin()), }), + owCommunityPoolSpendProposalOptions: owStrictObject().exactShape({ + title: ow.string, + description: ow.string, + recipient: ow.string, + amount: ow.array.ofType(owCoin()), + }), }; const proposalContentValidatorFn = (val: object) => ({ 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..d548df49 --- /dev/null +++ b/lib/src/transaction/msg/v2/gov/proposal/v2.CommunityPoolSpendProposal.spec.ts @@ -0,0 +1,121 @@ +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 TextProposal 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 TextProposal 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 TextProposal 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 textProposalContent = new cro.v2.gov.proposal.CommunityPoolSpendProposalV2({ + title: 'Text Proposal Title', + description: 'Lorem Ipsum ... Checking cancel software upgrade', + recipient: 'tcro1x07kkkepfj2hl8etlcuqhej7jj6myqrp48y4hg', + amount: [coin], + }); + + const TextProposalChangeParam = new cro.gov.MsgSubmitProposal({ + proposer: 'tcro14sh490wk79dltea4udk95k7mw40wmvf77p0l5a', + initialDeposit: coin, + content: textProposalContent, + }); + + const anySigner = { + publicKey: keyPair.getPubKey(), + accountNumber: new Big(6), + accountSequence: new Big(0), + }; + + const rawTx = new cro.RawTransaction(); + + const signableTx = rawTx.appendMessage(TextProposalChangeParam).addSigner(anySigner).toSignable(); + + const signedTx = signableTx.setSignature(0, keyPair.sign(signableTx.toSignDocumentHash(0))).toSigned(); + + const signedTxHex = signedTx.getHexEncoded(); + expect(signedTx.getTxHash()).to.be.eq('BE098DD0B77EC0AF5E57CB4CEBD51B163F3638ADF1AE3FE15F3C3ABACC7E0994'); + expect(signedTxHex).to.be.eql( + '0abf020abc020a252f636f736d6f732e676f762e763162657461312e4d73675375626d697450726f706f73616c1292020ac9010a372f636f736d6f732e646973747269627574696f6e2e763162657461312e436f6d6d756e697479506f6f6c5370656e6450726f706f73616c128d010a13546578742050726f706f73616c205469746c6512304c6f72656d20497073756d202e2e2e20436865636b696e672063616e63656c20736f66747761726520757067726164651a2b7463726f317830376b6b6b6570666a32686c3865746c63757168656a376a6a366d7971727034387934686722170a08626173657463726f120b313230303030303030303012170a08626173657463726f120b31323030303030303030301a2b7463726f31347368343930776b3739646c7465613475646b39356b376d773430776d7666373770306c356112580a500a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a210280c5e37a2bc3e68cc7c4aac78eac8c769cf58ce269ecd4307427aa16c2ba05a412040a0208011800120410c09a0c1a4095f0e26e67968cb8cbb29a259636c8afeeac19e215e9481fe6d929dfd0d34b0515b02005ff71034d713aa8ac8cbcabd9fdaacc39b3236075bf5dfe4d8d64862f', + ); + }); + describe('fromCosmosJSON', function () { + it('should throw Error if the JSON is not a TextProposal', 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 TextProposal 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 TextProposal = cro.v2.gov.proposal.CommunityPoolSpendProposalV2.fromCosmosMsgJSON( + json, + CroNetwork.Testnet, + ); + + expect(TextProposal.title).to.eql('Text Proposal Title'); + + expect(TextProposal.description).to.eql('Lorem Ipsum ... Checking text proposal'); + expect(TextProposal.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..b509ed73 --- /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[]; +}