Merge pull request #1262 from cosmos/fee-payer-simon

Add StdFee.granter support (for feegrant)
This commit is contained in:
Simon Warta 2022-09-07 18:11:57 +02:00 committed by GitHub
commit efc2b4e99d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 232 additions and 13 deletions

View File

@ -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

View File

@ -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;
}
/**

View File

@ -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);

View File

@ -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({

View File

@ -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({

View File

@ -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,
),
};

View File

@ -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,
);

View File

@ -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,
);

View File

@ -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();

View File

@ -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<QueryAllowanceResponse>;
readonly allowances: (grantee: string, paginationKey?: Uint8Array) => Promise<QueryAllowancesResponse>;
};
}
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;
},
},
};
}

View File

@ -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,

View File

@ -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", () => {

View File

@ -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({

View File

@ -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);

View File

@ -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({

View File

@ -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,
),
};