Merge pull request #856 from cosmos/add-gov-extension

Add GovExtension and add gov message types
This commit is contained in:
Simon Warta 2021-07-28 23:47:15 +02:00 committed by GitHub
commit 7ef4ee30aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 555 additions and 11 deletions

View File

@ -25,6 +25,8 @@ and this project adheres to
- @cosmjs/faucet: Add new configuration variable `FAUCET_PATH_PATTERN` to
configure the HD path of the faucet accounts ([#832]).
- @cosmjs/cosmwasm-stargate: Add field `ibcPortId` to `Contract` ([#836]).
- @cosmjs/stargate: Add `GovExtension` for query client.
- @cosmjs/stargate: Add support for `MsgSubmitProposal` and `MsgVote`.
[#832]: https://github.com/cosmos/cosmjs/issues/832
[#836]: https://github.com/cosmos/cosmjs/issues/836

View File

@ -85,10 +85,10 @@ export function setupWasmExtension(base: QueryClient): WasmExtension {
return queryService.Code(request);
},
listContractsByCodeId: async (id: number, paginationKey?: Uint8Array) => {
const pagination = {
const request = {
codeId: Long.fromNumber(id),
pagination: createPagination(paginationKey),
};
const request = { ...pagination, codeId: Long.fromNumber(id) };
return queryService.ContractsByCode(request);
},
getContractInfo: async (address: string) => {
@ -97,18 +97,18 @@ export function setupWasmExtension(base: QueryClient): WasmExtension {
},
getContractCodeHistory: async (address: string, paginationKey?: Uint8Array) => {
const pagination = {
const request = {
address: address,
pagination: createPagination(paginationKey),
};
const request = { ...pagination, address: address };
return queryService.ContractHistory(request);
},
getAllContractState: async (address: string, paginationKey?: Uint8Array) => {
const pagination = {
const request = {
address: address,
pagination: createPagination(paginationKey),
};
const request = { ...pagination, address: address };
return queryService.AllContractState(request);
},

View File

@ -1,6 +1,7 @@
import { EncodeObject } from "@cosmjs/proto-signing";
import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx";
import { MsgWithdrawDelegatorReward } from "cosmjs-types/cosmos/distribution/v1beta1/tx";
import { MsgSubmitProposal, MsgVote } from "cosmjs-types/cosmos/gov/v1beta1/tx";
import { MsgDelegate, MsgUndelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx";
import { MsgTransfer } from "cosmjs-types/ibc/applications/transfer/v1/tx";
@ -59,3 +60,23 @@ export function isMsgTransferEncodeObject(
): encodeObject is MsgTransferEncodeObject {
return (encodeObject as MsgTransferEncodeObject).typeUrl === "/ibc.applications.transfer.v1.MsgTransfer";
}
export interface MsgSubmitProposalEncodeObject extends EncodeObject {
readonly typeUrl: "/cosmos.gov.v1beta1.MsgSubmitProposal";
readonly value: Partial<MsgSubmitProposal>;
}
export function isMsgSubmitProposalEncodeObject(
encodeObject: EncodeObject,
): encodeObject is MsgSubmitProposalEncodeObject {
return (encodeObject as MsgSubmitProposalEncodeObject).typeUrl === "/cosmos.gov.v1beta1.MsgSubmitProposal";
}
export interface MsgVoteEncodeObject extends EncodeObject {
readonly typeUrl: "/cosmos.gov.v1beta1.MsgVote";
readonly value: Partial<MsgVote>;
}
export function isMsgVoteEncodeObject(encodeObject: EncodeObject): encodeObject is MsgVoteEncodeObject {
return (encodeObject as MsgVoteEncodeObject).typeUrl === "/cosmos.gov.v1beta1.MsgVote";
}

View File

@ -42,13 +42,17 @@ export { AminoConverter, AminoTypes } from "./aminotypes";
export {
isMsgDelegateEncodeObject,
isMsgSendEncodeObject,
isMsgSubmitProposalEncodeObject,
isMsgTransferEncodeObject,
isMsgUndelegateEncodeObject,
isMsgVoteEncodeObject,
isMsgWithdrawDelegatorRewardEncodeObject,
MsgDelegateEncodeObject,
MsgSendEncodeObject,
MsgSubmitProposalEncodeObject,
MsgTransferEncodeObject,
MsgUndelegateEncodeObject,
MsgVoteEncodeObject,
MsgWithdrawDelegatorRewardEncodeObject,
} from "./encodeobjects";
export { calculateFee, GasPrice } from "./fee";
@ -60,12 +64,16 @@ export {
createPagination,
createProtobufRpcClient,
DistributionExtension,
GovExtension,
GovParamsType,
GovProposalId,
IbcExtension,
ProtobufRpcClient,
QueryClient,
setupAuthExtension,
setupBankExtension,
setupDistributionExtension,
setupGovExtension,
setupIbcExtension,
setupStakingExtension,
StakingExtension,

View File

@ -0,0 +1,355 @@
import { coin, coins, makeCosmoshubPath } from "@cosmjs/amino";
import { toAscii } from "@cosmjs/encoding";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { assert, sleep } from "@cosmjs/utils";
import { ProposalStatus, TextProposal, VoteOption } from "cosmjs-types/cosmos/gov/v1beta1/gov";
import { Any } from "cosmjs-types/google/protobuf/any";
import Long from "long";
import {
MsgDelegateEncodeObject,
MsgSubmitProposalEncodeObject,
MsgVoteEncodeObject,
} from "../encodeobjects";
import { SigningStargateClient } from "../signingstargateclient";
import { assertIsBroadcastTxSuccess } from "../stargateclient";
import {
defaultSigningClientOptions,
faucet,
nonNegativeIntegerMatcher,
pendingWithoutSimapp,
simapp,
simappEnabled,
validator,
} from "../testutils.spec";
import { GovExtension, setupGovExtension } from "./gov";
import { QueryClient } from "./queryclient";
import { longify } from "./utils";
async function makeClientWithGov(rpcUrl: string): Promise<[QueryClient & GovExtension, Tendermint34Client]> {
const tmClient = await Tendermint34Client.connect(rpcUrl);
return [QueryClient.withExtensions(tmClient, setupGovExtension), tmClient];
}
describe("GovExtension", () => {
const defaultFee = {
amount: coins(25000, "ucosm"),
gas: "1500000", // 1.5 million
};
const textProposal = TextProposal.fromPartial({
title: "Test Proposal",
description: "This proposal proposes to test whether this proposal passes",
});
const initialDeposit = coins(12300000, "ustake");
const delegationVoter1 = coin(424242, "ustake");
const delegationVoter2 = coin(777, "ustake");
const voter1Address = faucet.address1;
const voter2Address = faucet.address2;
let proposalId: string;
beforeAll(async () => {
if (simappEnabled()) {
const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, {
// Use address 1 and 2 instead of 0 to avoid conflicts with other delegation tests
// This must match `voterAddress` above.
hdPaths: [makeCosmoshubPath(1), makeCosmoshubPath(2)],
});
const client = await SigningStargateClient.connectWithSigner(
simapp.tendermintUrl,
wallet,
defaultSigningClientOptions,
);
const proposalMsg: MsgSubmitProposalEncodeObject = {
typeUrl: "/cosmos.gov.v1beta1.MsgSubmitProposal",
value: {
content: Any.fromPartial({
typeUrl: "/cosmos.gov.v1beta1.TextProposal",
value: Uint8Array.from(TextProposal.encode(textProposal).finish()),
}),
proposer: voter1Address,
initialDeposit: initialDeposit,
},
};
const proposalResult = await client.signAndBroadcast(
voter1Address,
[proposalMsg],
defaultFee,
"Test proposal for simd",
);
assertIsBroadcastTxSuccess(proposalResult);
const logs = JSON.parse(proposalResult.rawLog || "");
proposalId = logs[0].events
.find(({ type }: any) => type === "submit_proposal")
.attributes.find(({ key }: any) => key === "proposal_id").value;
assert(proposalId.match(nonNegativeIntegerMatcher));
// Voter 1
{
// My vote only counts when I delegate
if (!(await client.getDelegation(voter1Address, validator.validatorAddress))) {
const msgDelegate: MsgDelegateEncodeObject = {
typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
value: {
delegatorAddress: voter1Address,
validatorAddress: validator.validatorAddress,
amount: delegationVoter1,
},
};
const result = await client.signAndBroadcast(voter1Address, [msgDelegate], defaultFee);
assertIsBroadcastTxSuccess(result);
}
const voteMsg: MsgVoteEncodeObject = {
typeUrl: "/cosmos.gov.v1beta1.MsgVote",
value: {
proposalId: longify(proposalId),
voter: voter1Address,
option: VoteOption.VOTE_OPTION_YES,
},
};
const voteResult = await client.signAndBroadcast(voter1Address, [voteMsg], defaultFee);
assertIsBroadcastTxSuccess(voteResult);
}
// Voter 2
{
// My vote only counts when I delegate
if (!(await client.getDelegation(voter2Address, validator.validatorAddress))) {
const msgDelegate: MsgDelegateEncodeObject = {
typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
value: {
delegatorAddress: voter2Address,
validatorAddress: validator.validatorAddress,
amount: delegationVoter2,
},
};
const result = await client.signAndBroadcast(voter2Address, [msgDelegate], defaultFee);
assertIsBroadcastTxSuccess(result);
}
const voteMsg: MsgVoteEncodeObject = {
typeUrl: "/cosmos.gov.v1beta1.MsgVote",
value: {
proposalId: longify(proposalId),
voter: voter2Address,
option: VoteOption.VOTE_OPTION_NO_WITH_VETO,
},
};
const voteResult = await client.signAndBroadcast(voter2Address, [voteMsg], defaultFee);
assertIsBroadcastTxSuccess(voteResult);
}
await sleep(75); // wait until transactions are indexed
client.disconnect();
}
});
describe("params", () => {
it("works for deposit", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.params("deposit");
expect(response).toEqual(
jasmine.objectContaining({
depositParams: {
minDeposit: simapp.govMinDeposit,
maxDepositPeriod: {
seconds: Long.fromNumber(172800, false),
nanos: 0,
},
},
}),
);
tmClient.disconnect();
});
it("works for tallying", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.params("tallying");
expect(response).toEqual(
jasmine.objectContaining({
tallyParams: {
// Why the f*** are we getting binary values here?
quorum: toAscii("334000000000000000"), // 0.334
threshold: toAscii("500000000000000000"), // 0.5
vetoThreshold: toAscii("334000000000000000"), // 0.334
},
}),
);
tmClient.disconnect();
});
it("works for voting", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.params("voting");
expect(response).toEqual(
jasmine.objectContaining({
votingParams: {
votingPeriod: {
seconds: Long.fromNumber(172800, false),
nanos: 0,
},
},
}),
);
tmClient.disconnect();
});
});
describe("proposals", () => {
it("works", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.proposals(
ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD,
voter1Address,
voter1Address,
);
expect(response.proposals.length).toBeGreaterThanOrEqual(1);
expect(response.proposals[response.proposals.length - 1]).toEqual({
content: Any.fromPartial({
typeUrl: "/cosmos.gov.v1beta1.TextProposal",
value: Uint8Array.from(TextProposal.encode(textProposal).finish()),
}),
proposalId: longify(proposalId),
status: ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD,
finalTallyResult: { yes: "0", abstain: "0", no: "0", noWithVeto: "0" },
submitTime: jasmine.any(Date),
depositEndTime: jasmine.any(Date),
totalDeposit: initialDeposit,
votingStartTime: jasmine.any(Date),
votingEndTime: jasmine.any(Date),
});
tmClient.disconnect();
});
});
describe("proposal", () => {
it("works", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.proposal(proposalId);
expect(response.proposal).toEqual({
content: Any.fromPartial({
typeUrl: "/cosmos.gov.v1beta1.TextProposal",
value: Uint8Array.from(TextProposal.encode(textProposal).finish()),
}),
proposalId: longify(proposalId),
status: ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD,
finalTallyResult: { yes: "0", abstain: "0", no: "0", noWithVeto: "0" },
submitTime: jasmine.any(Date),
depositEndTime: jasmine.any(Date),
totalDeposit: initialDeposit,
votingStartTime: jasmine.any(Date),
votingEndTime: jasmine.any(Date),
});
tmClient.disconnect();
});
});
describe("deposits", () => {
it("works", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.deposits(proposalId);
expect(response.deposits).toEqual([
{
proposalId: longify(proposalId),
depositor: voter1Address,
amount: initialDeposit,
},
]);
tmClient.disconnect();
});
});
describe("deposit", () => {
it("works", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.deposit(proposalId, voter1Address);
expect(response.deposit).toEqual({
proposalId: longify(proposalId),
depositor: voter1Address,
amount: initialDeposit,
});
tmClient.disconnect();
});
});
describe("tally", () => {
it("works", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.tally(proposalId);
expect(response.tally).toEqual({
yes: delegationVoter1.amount,
abstain: "0",
no: "0",
noWithVeto: delegationVoter2.amount,
});
tmClient.disconnect();
});
});
describe("votes", () => {
it("works", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.votes(proposalId);
expect(response.votes).toEqual([
// why is vote 2 first?
{
proposalId: longify(proposalId),
voter: voter2Address,
option: VoteOption.VOTE_OPTION_NO_WITH_VETO,
},
{
proposalId: longify(proposalId),
voter: voter1Address,
option: VoteOption.VOTE_OPTION_YES,
},
]);
tmClient.disconnect();
});
});
describe("vote", () => {
it("works", async () => {
pendingWithoutSimapp();
const [client, tmClient] = await makeClientWithGov(simapp.tendermintUrl);
const response = await client.gov.vote(proposalId, voter1Address);
expect(response.vote).toEqual({
voter: voter1Address,
proposalId: longify(proposalId),
option: VoteOption.VOTE_OPTION_YES,
});
tmClient.disconnect();
});
});
});

View File

@ -0,0 +1,107 @@
import { Uint64 } from "@cosmjs/math";
import { ProposalStatus } from "cosmjs-types/cosmos/gov/v1beta1/gov";
import {
QueryClientImpl,
QueryDepositResponse,
QueryDepositsResponse,
QueryParamsResponse,
QueryProposalResponse,
QueryProposalsResponse,
QueryTallyResultResponse,
QueryVoteResponse,
QueryVotesResponse,
} from "cosmjs-types/cosmos/gov/v1beta1/query";
import Long from "long";
import { QueryClient } from "./queryclient";
import { createPagination, createProtobufRpcClient, longify } from "./utils";
export type GovParamsType = "deposit" | "tallying" | "voting";
export type GovProposalId = string | number | Long | Uint64;
export interface GovExtension {
readonly gov: {
readonly params: (parametersType: GovParamsType) => Promise<QueryParamsResponse>;
readonly proposals: (
proposalStatus: ProposalStatus,
depositor: string,
voter: string,
) => Promise<QueryProposalsResponse>;
readonly proposal: (proposalId: GovProposalId) => Promise<QueryProposalResponse>;
readonly deposits: (proposalId: GovProposalId) => Promise<QueryDepositsResponse>;
readonly deposit: (proposalId: GovProposalId, depositorAddress: string) => Promise<QueryDepositResponse>;
readonly tally: (proposalId: GovProposalId) => Promise<QueryTallyResultResponse>;
readonly votes: (proposalId: GovProposalId) => Promise<QueryVotesResponse>;
readonly vote: (proposalId: GovProposalId, voterAddress: string) => Promise<QueryVoteResponse>;
};
}
export function setupGovExtension(base: QueryClient): GovExtension {
const rpc = createProtobufRpcClient(base);
// Use this service to get easy typed access to query methods
// This cannot be used for proof verification
const queryService = new QueryClientImpl(rpc);
return {
gov: {
params: async (parametersType: GovParamsType) => {
const response = await queryService.Params({ paramsType: parametersType });
return response;
},
proposals: async (
proposalStatus: ProposalStatus,
depositorAddress: string,
voterAddress: string,
paginationKey?: Uint8Array,
) => {
const response = await queryService.Proposals({
proposalStatus,
depositor: depositorAddress,
voter: voterAddress,
pagination: createPagination(paginationKey),
});
return response;
},
proposal: async (proposalId: GovProposalId) => {
const response = await queryService.Proposal({ proposalId: longify(proposalId) });
return response;
},
deposits: async (proposalId: GovProposalId, paginationKey?: Uint8Array) => {
const response = await queryService.Deposits({
proposalId: longify(proposalId),
pagination: createPagination(paginationKey),
});
return response;
},
deposit: async (proposalId: GovProposalId, depositorAddress: string) => {
const response = await queryService.Deposit({
proposalId: longify(proposalId),
depositor: depositorAddress,
});
return response;
},
tally: async (proposalId: GovProposalId) => {
const response = await queryService.TallyResult({
proposalId: longify(proposalId),
});
return response;
},
votes: async (proposalId: GovProposalId, paginationKey?: Uint8Array) => {
const response = await queryService.Votes({
proposalId: longify(proposalId),
pagination: createPagination(paginationKey),
});
return response;
},
vote: async (proposalId: GovProposalId, voterAddress: string) => {
const response = await queryService.Vote({
proposalId: longify(proposalId),
voter: voterAddress,
});
return response;
},
},
};
}

View File

@ -7,6 +7,7 @@ export { QueryClient } from "./queryclient";
export { AuthExtension, setupAuthExtension } from "./auth";
export { BankExtension, setupBankExtension } from "./bank";
export { DistributionExtension, setupDistributionExtension } from "./distribution";
export { setupGovExtension, GovExtension, GovProposalId, GovParamsType } from "./gov";
export { IbcExtension, setupIbcExtension } from "./ibc";
export { setupStakingExtension, StakingExtension } from "./staking";
export { createPagination, createProtobufRpcClient, ProtobufRpcClient } from "./utils";

View File

@ -1,4 +1,5 @@
import { Bech32 } from "@cosmjs/encoding";
import { Uint64 } from "@cosmjs/math";
import { PageRequest } from "cosmjs-types/cosmos/base/query/v1beta1/pagination";
import Long from "long";
@ -13,6 +14,13 @@ export function toAccAddress(address: string): Uint8Array {
return Bech32.decode(address).data;
}
/**
* If paginationKey is set, return a `PageRequest` with the given key.
* If paginationKey is unset, return `undefined`.
*
* Use this with a query response's pagination next key to
* request the next page.
*/
export function createPagination(paginationKey?: Uint8Array): PageRequest | undefined {
return paginationKey
? {
@ -36,3 +44,12 @@ export function createProtobufRpcClient(base: QueryClient): ProtobufRpcClient {
},
};
}
/**
* Takes a uint64 value as string, number, Long or Uint64 and returns an unsigned Long instance
* of it.
*/
export function longify(value: string | number | Long | Uint64): Long {
const checkedValue = Uint64.fromString(value.toString());
return Long.fromBytesBE([...checkedValue.toBytesBigEndian()], true);
}

View File

@ -22,6 +22,7 @@ import {
MsgWithdrawDelegatorReward,
MsgWithdrawValidatorCommission,
} from "cosmjs-types/cosmos/distribution/v1beta1/tx";
import { MsgSubmitProposal, MsgVote } from "cosmjs-types/cosmos/gov/v1beta1/tx";
import {
MsgBeginRedelegate,
MsgCreateValidator,
@ -75,6 +76,8 @@ export const defaultRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [
["/cosmos.distribution.v1beta1.MsgSetWithdrawAddress", MsgSetWithdrawAddress],
["/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward", MsgWithdrawDelegatorReward],
["/cosmos.distribution.v1beta1.MsgWithdrawValidatorCommission", MsgWithdrawValidatorCommission],
["/cosmos.gov.v1beta1.MsgSubmitProposal", MsgSubmitProposal],
["/cosmos.gov.v1beta1.MsgVote", MsgVote],
["/cosmos.staking.v1beta1.MsgBeginRedelegate", MsgBeginRedelegate],
["/cosmos.staking.v1beta1.MsgCreateValidator", MsgCreateValidator],
["/cosmos.staking.v1beta1.MsgDelegate", MsgDelegate],

View File

@ -7,7 +7,15 @@ import { MsgData } from "cosmjs-types/cosmos/base/abci/v1beta1/abci";
import { Coin } from "cosmjs-types/cosmos/base/v1beta1/coin";
import { Account, accountFromAny } from "./accounts";
import { AuthExtension, BankExtension, QueryClient, setupAuthExtension, setupBankExtension } from "./queries";
import {
AuthExtension,
BankExtension,
QueryClient,
setupAuthExtension,
setupBankExtension,
setupStakingExtension,
StakingExtension,
} from "./queries";
import {
isSearchByHeightQuery,
isSearchBySentFromOrToQuery,
@ -138,7 +146,7 @@ export interface PrivateStargateClient {
export class StargateClient {
private readonly tmClient: Tendermint34Client | undefined;
private readonly queryClient: (QueryClient & AuthExtension & BankExtension) | undefined;
private readonly queryClient: (QueryClient & AuthExtension & BankExtension & StakingExtension) | undefined;
private chainId: string | undefined;
public static async connect(endpoint: string): Promise<StargateClient> {
@ -149,7 +157,12 @@ export class StargateClient {
protected constructor(tmClient: Tendermint34Client | undefined) {
if (tmClient) {
this.tmClient = tmClient;
this.queryClient = QueryClient.withExtensions(tmClient, setupAuthExtension, setupBankExtension);
this.queryClient = QueryClient.withExtensions(
tmClient,
setupAuthExtension,
setupBankExtension,
setupStakingExtension,
);
}
}
@ -166,11 +179,11 @@ export class StargateClient {
return this.tmClient;
}
protected getQueryClient(): (QueryClient & AuthExtension & BankExtension) | undefined {
protected getQueryClient(): (QueryClient & AuthExtension & BankExtension & StakingExtension) | undefined {
return this.queryClient;
}
protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension {
protected forceGetQueryClient(): QueryClient & AuthExtension & BankExtension & StakingExtension {
if (!this.queryClient) {
throw new Error("Query client not available. You cannot use online functionality in offline mode.");
}
@ -254,6 +267,22 @@ export class StargateClient {
return this.forceGetQueryClient().bank.allBalances(address);
}
public async getDelegation(delegatorAddress: string, validatorAddress: string): Promise<Coin | null> {
let delegatedAmount: Coin | undefined;
try {
delegatedAmount = (
await this.forceGetQueryClient().staking.delegation(delegatorAddress, validatorAddress)
).delegationResponse?.balance;
} catch (e) {
if (e.toString().includes("key not found")) {
// ignore, `delegatedAmount` remains undefined
} else {
throw e;
}
}
return delegatedAmount || null;
}
public async getTx(id: string): Promise<IndexedTx | null> {
const results = await this.txsQuery(`tx.hash='${id}'`);
return results[0] ?? null;

View File

@ -61,6 +61,7 @@ export const simapp = {
denomFee: "ucosm",
blockTime: 1_000, // ms
totalSupply: 21000000000, // ucosm
govMinDeposit: coins(10000000, "ustake"),
};
export const slowSimapp = {