diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ead672e..737266f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,8 @@ and this project adheres to - @cosmjs/stargate: `BankExtension.totalSupply` now takes a pagination key argument and returns the full `QueryTotalSupplyResponse` including the next pagination key ([#1095]). +- @cosmjs/proto-signing: `makeAuthInfoBytes` now expects a fee granter and fee + payer argument in position 4 and 5. [#1131]: https://github.com/cosmos/cosmjs/pull/1131 [#1168]: https://github.com/cosmos/cosmjs/pull/1168 diff --git a/packages/amino/src/signdoc.ts b/packages/amino/src/signdoc.ts index 54bf86b0..2348353e 100644 --- a/packages/amino/src/signdoc.ts +++ b/packages/amino/src/signdoc.ts @@ -12,6 +12,10 @@ export interface AminoMsg { export interface StdFee { readonly amount: readonly Coin[]; readonly gas: string; + /** The granter address that is used for paying with feegrants */ + readonly granter?: string; + /** The fee payer address. The payer must have signed the transaction. */ + readonly payer?: string; } /** diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.searchtx.spec.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.searchtx.spec.ts index f65e2f48..18a8211d 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.searchtx.spec.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.searchtx.spec.ts @@ -79,7 +79,9 @@ async function sendTokens( }, ]; const gasLimit = 200000; - const authInfoBytes = makeAuthInfoBytes([{ pubkey, sequence }], feeAmount, gasLimit); + const feeGranter = undefined; + const feePayer = undefined; + const authInfoBytes = makeAuthInfoBytes([{ pubkey, sequence }], feeAmount, gasLimit, feeGranter, feePayer); const chainId = await client.getChainId(); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); diff --git a/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts b/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts index 669602dd..25fec479 100644 --- a/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts +++ b/packages/cosmwasm-stargate/src/cosmwasmclient.spec.ts @@ -203,7 +203,15 @@ describe("CosmWasmClient", () => { }; const txBodyBytes = registry.encode(txBody); const gasLimit = Int53.fromString(fee.gas).toNumber(); - const authInfoBytes = makeAuthInfoBytes([{ pubkey, sequence }], fee.amount, gasLimit); + const feeGranter = undefined; + const feePayer = undefined; + const authInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence }], + fee.amount, + gasLimit, + feeGranter, + feePayer, + ); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); const { signed, signature } = await wallet.signDirect(alice.address0, signDoc); const txRaw = TxRaw.fromPartial({ diff --git a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts index 2065437c..9fe64565 100644 --- a/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts +++ b/packages/cosmwasm-stargate/src/signingcosmwasmclient.ts @@ -586,6 +586,8 @@ export class SigningCosmWasmClient extends CosmWasmClient { [{ pubkey, sequence: signedSequence }], signed.fee.amount, signedGasLimit, + signed.fee.granter, + signed.fee.payer, signMode, ); return TxRaw.fromPartial({ @@ -619,7 +621,13 @@ export class SigningCosmWasmClient extends CosmWasmClient { }; const txBodyBytes = this.registry.encode(txBody); const gasLimit = Int53.fromString(fee.gas).toNumber(); - const authInfoBytes = makeAuthInfoBytes([{ pubkey, sequence }], fee.amount, gasLimit); + const authInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence }], + fee.amount, + gasLimit, + fee.granter, + fee.payer, + ); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc); return TxRaw.fromPartial({ diff --git a/packages/cosmwasm-stargate/src/testutils.spec.ts b/packages/cosmwasm-stargate/src/testutils.spec.ts index c4166443..e40b8b5b 100644 --- a/packages/cosmwasm-stargate/src/testutils.spec.ts +++ b/packages/cosmwasm-stargate/src/testutils.spec.ts @@ -225,6 +225,8 @@ export class ModifyingDirectSecp256k1HdWallet extends DirectSecp256k1HdWallet { })); const modifiedFeeAmount = coins(3000, "ucosm"); const modifiedGasLimit = 333333; + const modifiedFeeGranter = undefined; + const modifiedFeePayer = undefined; const modifiedSignDoc = { ...signDoc, bodyBytes: Uint8Array.from(TxBody.encode(modifiedTxBody).finish()), @@ -232,6 +234,8 @@ export class ModifyingDirectSecp256k1HdWallet extends DirectSecp256k1HdWallet { signers, modifiedFeeAmount, modifiedGasLimit, + modifiedFeeGranter, + modifiedFeePayer, SignMode.SIGN_MODE_DIRECT, ), }; diff --git a/packages/proto-signing/src/directsecp256k1hdwallet.spec.ts b/packages/proto-signing/src/directsecp256k1hdwallet.spec.ts index 0b9122f3..24757783 100644 --- a/packages/proto-signing/src/directsecp256k1hdwallet.spec.ts +++ b/packages/proto-signing/src/directsecp256k1hdwallet.spec.ts @@ -261,10 +261,12 @@ describe("DirectSecp256k1HdWallet", () => { }; const fee = coins(2000, "ucosm"); const gasLimit = 200000; + const feeGranter = undefined; + const feePayer = undefined; const chainId = "simd-testing"; const signDoc = makeSignDoc( fromHex(bodyBytes), - makeAuthInfoBytes([{ pubkey, sequence }], fee, gasLimit), + makeAuthInfoBytes([{ pubkey, sequence }], fee, gasLimit, feeGranter, feePayer), chainId, accountNumber, ); diff --git a/packages/proto-signing/src/directsecp256k1wallet.spec.ts b/packages/proto-signing/src/directsecp256k1wallet.spec.ts index ab42c726..58af6c3a 100644 --- a/packages/proto-signing/src/directsecp256k1wallet.spec.ts +++ b/packages/proto-signing/src/directsecp256k1wallet.spec.ts @@ -44,9 +44,11 @@ describe("DirectSecp256k1Wallet", () => { const fee = coins(2000, "ucosm"); const gasLimit = 200000; const chainId = "simd-testing"; + const feePayer = undefined; + const feeGranter = undefined; const signDoc = makeSignDoc( fromHex(bodyBytes), - makeAuthInfoBytes([{ pubkey, sequence }], fee, gasLimit), + makeAuthInfoBytes([{ pubkey, sequence }], fee, gasLimit, feeGranter, feePayer), chainId, accountNumber, ); diff --git a/packages/proto-signing/src/signing.ts b/packages/proto-signing/src/signing.ts index 73dec600..7ce7e476 100644 --- a/packages/proto-signing/src/signing.ts +++ b/packages/proto-signing/src/signing.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { assert } from "@cosmjs/utils"; import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing"; import { AuthInfo, SignDoc, SignerInfo } from "cosmjs-types/cosmos/tx/v1beta1/tx"; @@ -34,13 +35,24 @@ export function makeAuthInfoBytes( signers: ReadonlyArray<{ readonly pubkey: Any; readonly sequence: number }>, feeAmount: readonly Coin[], gasLimit: number, + feeGranter: string | undefined, + feePayer: string | undefined, signMode = SignMode.SIGN_MODE_DIRECT, ): Uint8Array { + // Required arguments 4 and 5 were added in CosmJS 0.29. Use runtime checks to help our non-TS users. + assert( + feeGranter === undefined || typeof feeGranter === "string", + "feeGranter must be undefined or string", + ); + assert(feePayer === undefined || typeof feePayer === "string", "feePayer must be undefined or string"); + const authInfo = { signerInfos: makeSignerInfos(signers, signMode), fee: { amount: [...feeAmount], gasLimit: Long.fromNumber(gasLimit), + granter: feeGranter, + payer: feePayer, }, }; return AuthInfo.encode(AuthInfo.fromPartial(authInfo)).finish(); diff --git a/packages/stargate/src/modules/feegrant/queries.ts b/packages/stargate/src/modules/feegrant/queries.ts new file mode 100644 index 00000000..f37634cf --- /dev/null +++ b/packages/stargate/src/modules/feegrant/queries.ts @@ -0,0 +1,40 @@ +import { + QueryAllowanceResponse, + QueryAllowancesResponse, + QueryClientImpl, +} from "cosmjs-types/cosmos/feegrant/v1beta1/query"; + +import { createPagination, createProtobufRpcClient, QueryClient } from "../../queryclient"; + +export interface FeegrantExtension { + readonly feegrant: { + readonly allowance: (granter: string, grantee: string) => Promise; + readonly allowances: (grantee: string, paginationKey?: Uint8Array) => Promise; + }; +} + +export function setupFeegrantExtension(base: QueryClient): FeegrantExtension { + // Use this service to get easy typed access to query methods + // This cannot be used for proof verification + const rpc = createProtobufRpcClient(base); + const queryService = new QueryClientImpl(rpc); + + return { + feegrant: { + allowance: async (granter: string, grantee: string) => { + const response = await queryService.Allowance({ + granter: granter, + grantee: grantee, + }); + return response; + }, + allowances: async (grantee: string, paginationKey?: Uint8Array) => { + const response = await queryService.Allowances({ + grantee: grantee, + pagination: createPagination(paginationKey), + }); + return response; + }, + }, + }; +} diff --git a/packages/stargate/src/modules/index.ts b/packages/stargate/src/modules/index.ts index e96cf863..0a31a5f7 100644 --- a/packages/stargate/src/modules/index.ts +++ b/packages/stargate/src/modules/index.ts @@ -39,6 +39,7 @@ export { } from "./evidence/aminomessages"; export { createFreegrantAminoConverters } from "./feegrant/aminomessages"; export { feegrantTypes } from "./feegrant/messages"; +export { FeegrantExtension, setupFeegrantExtension } from "./feegrant/queries"; export { AminoMsgDeposit, AminoMsgSubmitProposal, diff --git a/packages/stargate/src/signingstargateclient.spec.ts b/packages/stargate/src/signingstargateclient.spec.ts index 96789b34..a138e3e4 100644 --- a/packages/stargate/src/signingstargateclient.spec.ts +++ b/packages/stargate/src/signingstargateclient.spec.ts @@ -1,16 +1,33 @@ /* eslint-disable @typescript-eslint/naming-convention,no-bitwise */ import { Secp256k1HdWallet } from "@cosmjs/amino"; -import { coin, coins, decodeTxRaw, DirectSecp256k1HdWallet, Registry } from "@cosmjs/proto-signing"; +import { + coin, + coins, + decodeTxRaw, + DirectSecp256k1HdWallet, + makeCosmoshubPath, + Registry, +} from "@cosmjs/proto-signing"; +import { Tendermint34Client } from "@cosmjs/tendermint-rpc"; import { assert, sleep } from "@cosmjs/utils"; import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin"; +import { BasicAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/feegrant"; +import { MsgGrantAllowance } from "cosmjs-types/cosmos/feegrant/v1beta1/tx"; import { DeepPartial, MsgDelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx"; import { AuthInfo, TxBody, TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx"; +import { Any } from "cosmjs-types/google/protobuf/any"; import Long from "long"; import protobuf from "protobufjs/minimal"; import { AminoTypes } from "./aminotypes"; -import { AminoMsgDelegate, MsgDelegateEncodeObject, MsgSendEncodeObject } from "./modules"; +import { + AminoMsgDelegate, + MsgDelegateEncodeObject, + MsgSendEncodeObject, + setupFeegrantExtension, +} from "./modules"; +import { QueryClient } from "./queryclient"; import { PrivateSigningStargateClient, SigningStargateClient } from "./signingstargateclient"; import { assertIsDeliverTxFailure, assertIsDeliverTxSuccess, isDeliverTxFailure } from "./stargateclient"; import { @@ -140,6 +157,78 @@ describe("SigningStargateClient", () => { const after = await client.getBalance(beneficiaryAddress, "ucosm"); expect(after).toEqual(amount[0]); }); + + it("works with feegrant granter", async () => { + pendingWithoutSimapp(); + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, { + hdPaths: [makeCosmoshubPath(0), makeCosmoshubPath(1)], + }); + const [{ address: signer }, { address: payer }] = await wallet.getAccounts(); + const client = await SigningStargateClient.connectWithSigner( + simapp.tendermintUrl, + wallet, + defaultSigningClientOptions, + ); + + const tmClient = await Tendermint34Client.connect(simapp.tendermintUrl); + const queryClient = QueryClient.withExtensions(tmClient, setupFeegrantExtension); + let allowanceExists: boolean; + try { + const _existingAllowance = await queryClient.feegrant.allowance(payer, signer); + allowanceExists = true; + } catch { + allowanceExists = false; + } + + if (!allowanceExists) { + // Create feegrant allowance + const allowance: Any = { + typeUrl: "/cosmos.feegrant.v1beta1.BasicAllowance", + value: Uint8Array.from( + BasicAllowance.encode({ + spendLimit: [ + { + denom: "ucosm", + amount: "1234567", + }, + ], + }).finish(), + ), + }; + const grantMsg = { + typeUrl: "/cosmos.feegrant.v1beta1.MsgGrantAllowance", + value: MsgGrantAllowance.fromPartial({ + granter: payer, + grantee: signer, + allowance: allowance, + }), + }; + const grantResult = await client.signAndBroadcast(payer, [grantMsg], "auto", "Create allowance"); + assertIsDeliverTxSuccess(grantResult); + } + + const balanceSigner1 = await client.getBalance(signer, "ucosm"); + const balancePayer1 = await client.getBalance(payer, "ucosm"); + + const sendAmount = coins(7890, "ucosm"); + const feeAmount = coins(4444, "ucosm"); + + // send + const result = await client.sendTokens(signer, makeRandomAddress(), sendAmount, { + amount: feeAmount, + gas: "120000", + granter: payer, + }); + assertIsDeliverTxSuccess(result); + + const balanceSigner2 = await client.getBalance(signer, "ucosm"); + const balancePayer2 = await client.getBalance(payer, "ucosm"); + + const diffSigner = Number(BigInt(balanceSigner1.amount) - BigInt(balanceSigner2.amount)); + const diffPayer = Number(BigInt(balancePayer1.amount) - BigInt(balancePayer2.amount)); + expect(diffSigner).toEqual(7890); // the send amount + expect(diffPayer).toEqual(4444); // the fee + }); }); describe("sendIbcTokens", () => { diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index 9dec3293..23641688 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -366,6 +366,8 @@ export class SigningStargateClient extends StargateClient { [{ pubkey, sequence: signedSequence }], signed.fee.amount, signedGasLimit, + signed.fee.granter, + signed.fee.payer, signMode, ); return TxRaw.fromPartial({ @@ -399,7 +401,13 @@ export class SigningStargateClient extends StargateClient { }; const txBodyBytes = this.registry.encode(txBodyEncodeObject); const gasLimit = Int53.fromString(fee.gas).toNumber(); - const authInfoBytes = makeAuthInfoBytes([{ pubkey, sequence }], fee.amount, gasLimit); + const authInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence }], + fee.amount, + gasLimit, + fee.granter, + fee.payer, + ); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc); return TxRaw.fromPartial({ diff --git a/packages/stargate/src/stargateclient.searchtx.spec.ts b/packages/stargate/src/stargateclient.searchtx.spec.ts index 02f0b1b4..01d0e27a 100644 --- a/packages/stargate/src/stargateclient.searchtx.spec.ts +++ b/packages/stargate/src/stargateclient.searchtx.spec.ts @@ -74,7 +74,9 @@ async function sendTokens( }, ]; const gasLimit = 200000; - const authInfoBytes = makeAuthInfoBytes([{ pubkey, sequence }], feeAmount, gasLimit); + const feeGranter = undefined; + const feePayer = undefined; + const authInfoBytes = makeAuthInfoBytes([{ pubkey, sequence }], feeAmount, gasLimit, feeGranter, feePayer); const chainId = await client.getChainId(); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); diff --git a/packages/stargate/src/stargateclient.spec.ts b/packages/stargate/src/stargateclient.spec.ts index 1b937656..641a31e8 100644 --- a/packages/stargate/src/stargateclient.spec.ts +++ b/packages/stargate/src/stargateclient.spec.ts @@ -364,7 +364,15 @@ describe("StargateClient", () => { const { accountNumber, sequence } = (await client.getSequence(address))!; const feeAmount = coins(2000, "ucosm"); const gasLimit = 200000; - const authInfoBytes = makeAuthInfoBytes([{ pubkey, sequence }], feeAmount, gasLimit); + const feeGranter = undefined; + const feePayer = undefined; + const authInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence }], + feeAmount, + gasLimit, + feeGranter, + feePayer, + ); const chainId = await client.getChainId(); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); @@ -421,7 +429,16 @@ describe("StargateClient", () => { const { accountNumber, sequence } = (await client.getSequence(address))!; const feeAmount = coins(2000, "ucosm"); const gasLimit = 200000; - const authInfoBytes = makeAuthInfoBytes([{ pubkey, sequence }], feeAmount, gasLimit, sequence); + const feeGranter = undefined; + const feePayer = undefined; + const authInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence }], + feeAmount, + gasLimit, + feeGranter, + feePayer, + sequence, + ); const chainId = await client.getChainId(); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, accountNumber); @@ -482,9 +499,17 @@ describe("StargateClient", () => { const chainId = await client.getChainId(); const feeAmount = coins(2000, "ucosm"); const gasLimit = 200000; + const feeGranter = undefined; + const feePayer = undefined; const { accountNumber: accountNumber1, sequence: sequence1 } = (await client.getSequence(address))!; - const authInfoBytes1 = makeAuthInfoBytes([{ pubkey, sequence: sequence1 }], feeAmount, gasLimit); + const authInfoBytes1 = makeAuthInfoBytes( + [{ pubkey, sequence: sequence1 }], + feeAmount, + gasLimit, + feeGranter, + feePayer, + ); const signDoc1 = makeSignDoc(txBodyBytes, authInfoBytes1, chainId, accountNumber1); const { signature: signature1 } = await wallet.signDirect(address, signDoc1); const txRaw1 = TxRaw.fromPartial({ @@ -498,7 +523,13 @@ describe("StargateClient", () => { assertIsDeliverTxSuccess(txResult); const { accountNumber: accountNumber2, sequence: sequence2 } = (await client.getSequence(address))!; - const authInfoBytes2 = makeAuthInfoBytes([{ pubkey, sequence: sequence2 }], feeAmount, gasLimit); + const authInfoBytes2 = makeAuthInfoBytes( + [{ pubkey, sequence: sequence2 }], + feeAmount, + gasLimit, + feeGranter, + feePayer, + ); const signDoc2 = makeSignDoc(txBodyBytes, authInfoBytes2, chainId, accountNumber2); const { signature: signature2 } = await wallet.signDirect(address, signDoc2); const txRaw2 = TxRaw.fromPartial({ diff --git a/packages/stargate/src/testutils.spec.ts b/packages/stargate/src/testutils.spec.ts index 47f33fdc..fb59b743 100644 --- a/packages/stargate/src/testutils.spec.ts +++ b/packages/stargate/src/testutils.spec.ts @@ -234,6 +234,8 @@ export class ModifyingDirectSecp256k1HdWallet extends DirectSecp256k1HdWallet { })); const modifiedFeeAmount = coins(3000, "ucosm"); const modifiedGasLimit = 333333; + const modifiedFeeGranter = undefined; + const modifiedFeePayer = undefined; const modifiedSignDoc = { ...signDoc, bodyBytes: Uint8Array.from(TxBody.encode(modifiedTxBody).finish()), @@ -241,6 +243,8 @@ export class ModifyingDirectSecp256k1HdWallet extends DirectSecp256k1HdWallet { signers, modifiedFeeAmount, modifiedGasLimit, + modifiedFeeGranter, + modifiedFeePayer, SignMode.SIGN_MODE_DIRECT, ), };