diff --git a/packages/stargate/src/modules/authz/aminomessages.spec.ts b/packages/stargate/src/modules/authz/aminomessages.spec.ts new file mode 100644 index 0000000000..8f17aca865 --- /dev/null +++ b/packages/stargate/src/modules/authz/aminomessages.spec.ts @@ -0,0 +1,237 @@ +import { MsgExec, MsgGrant, MsgRevoke } from "cosmjs-types/cosmos/authz/v1beta1/tx"; +import { SendAuthorization } from "cosmjs-types/cosmos/bank/v1beta1/authz"; +import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; + +import { AminoTypes } from "../../aminotypes"; +import { AminoMsgExec, AminoMsgGrant, AminoMsgRevoke, createAuthzAminoConverters } from "./aminomessages"; + +describe("Authz Amino Converters", () => { + describe("fromAmino", () => { + it("work with MsgGrant", () => { + // Define a sample AminoMsgGrant object + const msg: AminoMsgGrant = { + type: "cosmos-sdk/MsgGrant", + value: { + granter: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + grant: { + authorization: { + type: "cosmos-sdk/SendAuthorization", + value: { + spend_limit: [{ denom: "ustake", amount: "1000000" }], + allow_list: ["cosmos147auavf4tvghskslq2w65de0nh5dqdmljxc7kh"], + }, + }, + expiration: new Date("Mon Jan 19 1970 19:25:00 GMT+0800 (Indochina Time)") + .toISOString() + .replace(/\.000Z$/, "Z"), + }, + }, + }; + + const msgGrant = new AminoTypes(createAuthzAminoConverters()).fromAmino(msg); + const expectedValue: MsgGrant = { + granter: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + grant: { + authorization: { + typeUrl: "/cosmos.bank.v1beta1.SendAuthorization", + value: SendAuthorization.encode({ + spendLimit: [{ denom: "ustake", amount: "1000000" }], + allowList: ["cosmos147auavf4tvghskslq2w65de0nh5dqdmljxc7kh"], + }).finish(), + }, + expiration: { + seconds: BigInt(1596300), + nanos: 0, + }, + }, + }; + + expect(msgGrant).toEqual({ + typeUrl: "/cosmos.authz.v1beta1.MsgGrant", + value: expectedValue, + }); + }); + + it("work with MsgExec", () => { + // Define a sample AminoMsgExec object + const msg: AminoMsgExec = { + type: "cosmos-sdk/MsgExec", + value: { + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + msgs: [ + { + typeUrl: "cosmos-sdk/MsgSend", + value: MsgSend.encode({ + fromAddress: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + toAddress: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + amount: [{ denom: "ustake", amount: "1000000" }], + }).finish(), + }, + ], + }, + }; + + const msgExec = new AminoTypes(createAuthzAminoConverters()).fromAmino(msg); + const expectedValue: MsgExec = { + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + msgs: [ + { + typeUrl: "cosmos-sdk/MsgSend", + value: MsgSend.encode({ + fromAddress: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + toAddress: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + amount: [{ denom: "ustake", amount: "1000000" }], + }).finish(), + }, + ], + }; + + expect(msgExec).toEqual({ + typeUrl: "/cosmos.authz.v1beta1.MsgExec", + value: expectedValue, + }); + }); + + it("work with MsgRevoke", () => { + // Define a sample AminoMsgExec object + const msg: AminoMsgRevoke = { + type: "cosmos-sdk/MsgRevoke", + value: { + granter: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + msg_type_url: "cosmos-sdk/MsgSend", + }, + }; + + const msgRevoke = new AminoTypes(createAuthzAminoConverters()).fromAmino(msg); + const expectedValue: MsgRevoke = { + granter: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + msgTypeUrl: "cosmos-sdk/MsgSend", + }; + + expect(msgRevoke).toEqual({ + typeUrl: "/cosmos.authz.v1beta1.MsgRevoke", + value: expectedValue, + }); + }); + }); + + describe("toAmino", () => { + describe("fromAmino", () => { + it("work with MsgGrant", () => { + // Define a sample MsgGrant object + const msg: MsgGrant = { + granter: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + grant: { + authorization: { + typeUrl: "/cosmos.bank.v1beta1.SendAuthorization", + value: SendAuthorization.encode({ + spendLimit: [{ denom: "ustake", amount: "1000000" }], + allowList: ["cosmos147auavf4tvghskslq2w65de0nh5dqdmljxc7kh"], + }).finish(), + }, + expiration: { + seconds: BigInt(1596300), + nanos: 0, + }, + }, + }; + + const aminoMsg = new AminoTypes(createAuthzAminoConverters()).toAmino({ + typeUrl: "/cosmos.authz.v1beta1.MsgGrant", + value: msg, + }); + + const expectedValues: AminoMsgGrant = { + type: "cosmos-sdk/MsgGrant", + value: { + granter: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + grant: { + authorization: { + type: "cosmos-sdk/SendAuthorization", + value: { + spend_limit: [{ denom: "ustake", amount: "1000000" }], + allow_list: ["cosmos147auavf4tvghskslq2w65de0nh5dqdmljxc7kh"], + }, + }, + expiration: new Date("Mon Jan 19 1970 19:25:00 GMT+0800 (Indochina Time)") + .toISOString() + .replace(/\.000Z$/, "Z"), + }, + }, + }; + + expect(aminoMsg).toEqual(expectedValues); + }); + + it("work with MsgExec", () => { + // Define a sample MsgExec object + const msg: MsgExec = { + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + msgs: [ + { + typeUrl: "cosmos-sdk/MsgSend", + value: MsgSend.encode({ + fromAddress: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + toAddress: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + amount: [{ denom: "ustake", amount: "1000000" }], + }).finish(), + }, + ], + }; + const msgExec = new AminoTypes(createAuthzAminoConverters()).toAmino({ + typeUrl: "/cosmos.authz.v1beta1.MsgExec", + value: msg, + }); + + const expectedValues: AminoMsgExec = { + type: "cosmos-sdk/MsgExec", + value: { + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + msgs: [ + { + typeUrl: "cosmos-sdk/MsgSend", + value: MsgSend.encode({ + fromAddress: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + toAddress: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + amount: [{ denom: "ustake", amount: "1000000" }], + }).finish(), + }, + ], + }, + }; + + expect(msgExec).toEqual(expectedValues); + }); + + it("work with MsgRevoke", () => { + // Define a sample AminoMsgExec object + const msg: MsgRevoke = { + granter: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + msgTypeUrl: "cosmos-sdk/MsgSend", + }; + const msgRevoke = new AminoTypes(createAuthzAminoConverters()).toAmino({ + typeUrl: "/cosmos.authz.v1beta1.MsgRevoke", + value: msg, + }); + + const expectedValues: AminoMsgRevoke = { + type: "cosmos-sdk/MsgRevoke", + value: { + granter: "cosmos1p7v0km9ydt0y9nszlesjy8elzqc4n2v0w4xh2p", + grantee: "cosmos10dyr9899g6t0pelew4nvf4j5c3jcgv0r73qga5", + msg_type_url: "cosmos-sdk/MsgSend", + }, + }; + + expect(msgRevoke).toEqual(expectedValues); + }); + }); + }); +}); diff --git a/packages/stargate/src/modules/authz/aminomessages.ts b/packages/stargate/src/modules/authz/aminomessages.ts index b485788434..e31faeb91d 100644 --- a/packages/stargate/src/modules/authz/aminomessages.ts +++ b/packages/stargate/src/modules/authz/aminomessages.ts @@ -1,13 +1,234 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { AminoMsg } from "@cosmjs/amino"; +import { GenericAuthorization } from "cosmjs-types/cosmos/authz/v1beta1/authz"; +import { MsgExec, MsgGrant, MsgRevoke } from "cosmjs-types/cosmos/authz/v1beta1/tx"; +import { SendAuthorization } from "cosmjs-types/cosmos/bank/v1beta1/authz"; +import { StakeAuthorization } from "cosmjs-types/cosmos/staking/v1beta1/authz"; +import { Any } from "cosmjs-types/google/protobuf/any"; +import { Timestamp } from "cosmjs-types/google/protobuf/timestamp"; + +// eslint-disable-next-line import/no-cycle import { AminoConverters } from "../../aminotypes"; +interface AminoGrant { + authorization?: any; + expiration?: string; +} + +export interface AminoMsgGrant extends AminoMsg { + readonly type: "cosmos-sdk/MsgGrant"; + readonly value: { + /** Bech32 account address */ + readonly granter: string; + /** Bech32 account address */ + readonly grantee: string; + readonly grant: AminoGrant; + }; +} + +export interface AminoMsgExec extends AminoMsg { + readonly type: "cosmos-sdk/MsgExec"; + readonly value: { + /** Bech32 account address */ + readonly grantee: string; + readonly msgs: readonly Any[]; + }; +} + +export interface AminoMsgRevoke extends AminoMsg { + readonly type: "cosmos-sdk/MsgRevoke"; + readonly value: { + /** Bech32 account address */ + readonly granter: string; + /** Bech32 account address */ + readonly grantee: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + readonly msg_type_url: string; + }; +} + export function createAuthzAminoConverters(): AminoConverters { return { // For Cosmos SDK < 0.46 the Amino JSON codec was broken on chain and thus inaccessible. // Now this can be implemented for 0.46+ chains, see // https://github.com/cosmos/cosmjs/issues/1092 // - // "/cosmos.authz.v1beta1.MsgGrant": IMPLEMENT ME, - // "/cosmos.authz.v1beta1.MsgExec": IMPLEMENT ME, - // "/cosmos.authz.v1beta1.MsgRevoke": IMPLEMENT ME, + "/cosmos.authz.v1beta1.MsgGrant": { + aminoType: "cosmos-sdk/MsgGrant", + toAmino: ({ granter, grantee, grant }: MsgGrant) => { + if (!grant || !grant.authorization || grant.authorization.typeUrl === "") { + throw new Error(`Unsupported grant type: '${grant?.authorization?.typeUrl}'`); + } + let authorizationValue; + switch (grant.authorization.typeUrl) { + case "/cosmos.authz.v1beta1.GenericAuthorization": { + const generic = GenericAuthorization.decode(grant.authorization.value); + authorizationValue = { + type: "cosmos-sdk/GenericAuthorization", + value: { + msg: generic.msg, + }, + }; + break; + } + case "/cosmos.bank.v1beta1.SendAuthorization": { + const spend = SendAuthorization.decode(grant.authorization.value); + authorizationValue = { + type: "cosmos-sdk/SendAuthorization", + value: { + spend_limit: spend.spendLimit, + allow_list: spend.allowList, + }, + }; + break; + } + case "/cosmos.staking.v1beta1.StakeAuthorization": { + const stakeAuthorization = StakeAuthorization.decode(grant.authorization.value); + + if (stakeAuthorization) { + const isAllowListEmpty = stakeAuthorization.allowList?.address.length === 0; + const isDenyListEmpty = stakeAuthorization.denyList?.address.length === 0; + + if (isAllowListEmpty && isDenyListEmpty) { + throw new Error("Allow list and deny list can't be both empty"); + } else if (!isAllowListEmpty && !isDenyListEmpty) { + throw new Error("Can only set allow list or deny list at a time"); + } + } + + authorizationValue = { + type: "cosmos-sdk/StakeAuthorization", + value: { + max_tokens: stakeAuthorization.maxTokens, + allow_list: stakeAuthorization.allowList, + deny_list: stakeAuthorization.denyList, + authorization_type: stakeAuthorization.authorizationType, + }, + }; + break; + } + default: + throw new Error(`Unsupported grant type: '${grant.authorization.typeUrl}'`); + } + const expiration = grant.expiration?.seconds; + return { + granter, + grantee, + grant: { + authorization: authorizationValue, + expiration: expiration + ? new Date(Number(expiration) * 1000).toISOString().replace(/\.000Z$/, "Z") + : undefined, + }, + }; + }, + fromAmino: ({ + granter, + grantee, + grant, + }: { + granter: string; + grantee: string; + grant: any; + }): MsgGrant => { + const authorizationType = grant?.authorization?.type; + let authorizationValue; + switch (authorizationType) { + case "cosmos-sdk/GenericAuthorization": { + authorizationValue = { + typeUrl: "/cosmos.authz.v1beta1.GenericAuthorization", + value: GenericAuthorization.encode({ msg: grant.authorization.value.msg }).finish(), + }; + break; + } + case "cosmos-sdk/SendAuthorization": { + authorizationValue = { + typeUrl: "/cosmos.bank.v1beta1.SendAuthorization", + value: SendAuthorization.encode( + SendAuthorization.fromPartial({ + spendLimit: grant.authorization.value.spend_limit, + allowList: grant.authorization.value.allow_list, + }), + ).finish(), + }; + break; + } + case "cosmos-sdk/StakeAuthorization": { + authorizationValue = { + typeUrl: "/cosmos.staking.v1beta1.StakeAuthorization", + value: StakeAuthorization.encode( + StakeAuthorization.fromPartial({ + maxTokens: grant.authorization.value.max_token, + allowList: grant.authorization.value.allow_list, + denyList: grant.authorization.value.deny_list, + authorizationType: grant.authorization.value.authorization_type, + }), + ).finish(), + }; + break; + } + default: + throw new Error(`Unsupported grant type: '${grant?.authorization?.type}'`); + } + const expiration = grant.expiration ? Date.parse(grant.expiration) : undefined; + return MsgGrant.fromPartial({ + granter, + grantee, + grant: { + authorization: authorizationValue, + expiration: expiration + ? Timestamp.fromPartial({ + seconds: BigInt(expiration / 1000), + nanos: (expiration % 1000) * 1e6, + }) + : undefined, + }, + }); + }, + }, + "/cosmos.authz.v1beta1.MsgExec": { + aminoType: "cosmos-sdk/MsgExec", + toAmino: ({ grantee, msgs }: MsgExec) => { + if (msgs.length === 0) { + throw new Error("Messages list must not be empty"); + } + return MsgExec.fromPartial({ + grantee, + msgs, + }); + }, + fromAmino: ({ grantee, msgs }: { grantee: string; msgs: Any[] }): MsgExec => { + if (msgs.length === 0) { + throw new Error("Messages list must not be empty"); + } + return MsgExec.fromPartial({ grantee, msgs }); + }, + }, + "/cosmos.authz.v1beta1.MsgRevoke": { + aminoType: "cosmos-sdk/MsgRevoke", + toAmino: ({ granter, grantee, msgTypeUrl }: MsgRevoke) => ({ + granter, + grantee, + msg_type_url: msgTypeUrl, + }), + /* eslint-disable camelcase */ + fromAmino: ({ + granter, + grantee, + // eslint-disable-next-line @typescript-eslint/naming-convention + msg_type_url, + }: { + granter: string; + grantee: string; + // eslint-disable-next-line @typescript-eslint/naming-convention + msg_type_url: string; + }): MsgRevoke => + MsgRevoke.fromPartial({ + granter, + grantee, + msgTypeUrl: msg_type_url, + }), + /* eslint-enable camelcase */ + }, }; }