cosmos / cosmjs

The Swiss Army knife to power JavaScript based client solutions ranging from Web apps/explorers over browser extensions to server-side clients like faucets/scrapers.
https://cosmos.github.io/cosmjs/
Apache License 2.0
645 stars 330 forks source link

What is exact usage of “signDirect” and “sign”? #1481

Closed dovigod closed 11 months ago

dovigod commented 11 months ago

Hi, I’m currently developing with cosmjs sdk.

what Im doing is trying to map ethers method with cosmos methods.

While trying to map” ethers.signMessage” I found that there are two similar methods, which is

  1. SigningStargateClient.sign
  2. Direct256k1HdWallet.signDriect

since, I couldnt found definetions of above two methods, I tried to inspect codes of them in this repository, but couldn’t solve my questions.

I’ll be thankful for answers.

j0nl1 commented 11 months ago

In cosmos there are two types of message encoding, amino and protobuf, being Amino legacy but still maintained because is what ledger use to sign. Depending on what encoding you choose to prepare your tx, you will need to use one structure or another. For Amino encoding StdSignDoc is used and for Direct it is SignDoc. Thats why we differentiate between signAmino and signDirect, because these methods prepare the right encoding in CosmJS to be signed by the signer.

Method sign is just an intermediary method that check if the signer support direct or amino in case of support direct it always select direct. You can check OfflineSigner interface.

dovigod commented 11 months ago

@j0nl1 Thanks for reply!! So, to be sure, 'SigningStargateClient.sign' , will result exactly same with 'Direct256h1Wallet.signDirect' if I'm not using ledger right?

j0nl1 commented 11 months ago

If you are providing a direct signer when instantiate SigningStargateClient then it will call signDirect yes. Assuming you are providing Direct256h1Wallet then yes it will call signDirect method.

MbBrainz commented 11 months ago

I've experienced different behaviour than what you describe above @j0nl1. Even though I use proto msgs and keplr wallet, it seems to switch to Amino signing for some reason.

The case of sending a single message MsgDelegate there is no problem and it seems like the signer is using signDirect. (as it works without the aminoConverters added to the signing client, however, with 2 messages it behaves differently:

I create 2 messages: One is MsgDelegate and one is MsgGrant, where the message grant contains a staking authorization. As you can see from the imports below, the types that i use are all proto based types, not amino. However, when i sign using a CosmwasmSigningClient that has the proper registry setup, The sending the signAndBroadcast will fail before confirming with the following error: Type URL '/cosmos.authz.v1beta1.MsgGrant' does not exist in the Amino message type register. If you...

Is there a reason why the signing client asks for amino types instead of just accepting the prototypes?

import { MsgGrant } from 'cosmjs-types/cosmos/authz/v1beta1/tx'
import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin'
import { AuthorizationType, StakeAuthorization } from 'cosmjs-types/cosmos/staking/v1beta1/authz'
import { GenericAuthorization } from 'cosmjs-types/cosmos/authz/v1beta1/authz'
import { getUnixTime, parse } from 'date-fns'
import { EncodeObject } from '@cosmjs/proto-signing'
...
value: MsgGrant.fromPartial(value), // using from partial for creation of the messages
...

In case there is no clear explanation and you'd like to see more of the implementation im using, ive added a thinned version of the code down here ⬇️

Complete code blocks The tx is sent in the `useStake` hook defined in the bottom of the codeblock. ```ts /// grants.ts import { AminoTypes, SigningStargateClient } from '@cosmjs/stargate' import { MsgGrant } from 'cosmjs-types/cosmos/authz/v1beta1/tx' import { Coin } from 'cosmjs-types/cosmos/base/v1beta1/coin' import { AuthorizationType, StakeAuthorization } from 'cosmjs-types/cosmos/staking/v1beta1/authz' import { getUnixTime, parse } from 'date-fns' import { GenericAuthorization } from 'interchain-query/cosmos/authz/v1beta1/authz' import { EncodeObject } from '@cosmjs/proto-signing' interface Operator { address: string botAddress: string } export interface StakingGrantParams { granter: string grantee: Operator maxTokens: Coin | undefined expiryDate?: string // can be undefined if no expiry date: https://github.com/leapwallet/cosmos-extension/blob/967b87a243ea5ffecb36d99faa06c10b82290e11/packages/wallet-hooks/src/staking/useRestake.ts#L142C25-L142C25 } export function buildGrantMsg( type: string, granter: string, grantee: string, authValue: any, expiryDate?: string, ): EncodeObject { const value = { granter: granter, grantee: grantee, grant: { authorization: { typeUrl: type, value: authValue, }, expiration: expiryDate ? { seconds: getUnixTime(parse(expiryDate, 'dd/MM/yyyy', new Date())), nanos: 0, } : undefined, }, } return { typeUrl: '/cosmos.authz.v1beta1.MsgGrant', value: MsgGrant.fromPartial(value), } } /// for more like this: https://github.com/leapwallet/cosmos-extension/blob/967b87a243ea5ffecb36d99faa06c10b82290e11/packages/wallet-sdk/src/tx/msgs/cosmos.ts export function buildStakingGrantMsg(params: StakingGrantParams): EncodeObject { const { granter, grantee, maxTokens, expiryDate } = params const message = buildGrantMsg( '/cosmos.staking.v1beta1.StakeAuthorization', granter, grantee.botAddress, StakeAuthorization.encode( StakeAuthorization.fromPartial({ allowList: { address: [grantee.address] }, maxTokens: maxTokens, authorizationType: AuthorizationType.AUTHORIZATION_TYPE_DELEGATE, }), ).finish(), expiryDate, ) /// useTransaction.ts import { DeliverTxResponse, MsgExecuteContractEncodeObject } from '@cosmjs/cosmwasm-stargate' import { useMutation } from '@tanstack/react-query' import useToaster from './useToaster' import useWallet from './useWallet' import { useMemo } from 'react' type Transaction = { msgs: MsgExecuteContractEncodeObject[] | null onSuccess?: () => void } let mockTxResponse: DeliverTxResponse = { transactionHash: 'mockTxHash', data: undefined, height: 0, rawLog: '', gasUsed: 0, gasWanted: 0, code: 0, events: [], txIndex: 0, msgResponses: [], } async function mockTxbroadcast() { return new Promise((resolve) => { setTimeout(() => { resolve(mockTxResponse) }, 1000) }) } const useTransaction = ({ msgs, onSuccess }: Transaction) => { const toaster = useToaster() const mock = true const { isWalletConnected, getSigningCosmWasmClient, address } = useWallet() return useMutation( ['transaction', msgs, isWalletConnected, address], async () => { if (!address || !msgs || !isWalletConnected) throw new Error('Missing transaction parameters') const signingClient = await getSigningCosmWasmClient() if (mock) { await mockTxbroadcast() } const result = await signingClient?.signAndBroadcast(address, msgs!, 'auto', undefined) return result as DeliverTxResponse }, { onSuccess: (res: DeliverTxResponse) => { const { transactionHash } = res toaster.success({ message: 'Transaction Successful', txHash: transactionHash, }) onSuccess?.() }, onError: (error) => { console.error(error) toaster.error({ message: 'Transaction Failed', }) }, }, ) } export default useTransaction /// signerOptions.ts import { AminoTypes, defaultRegistryTypes, GasPrice } from '@cosmjs/stargate' import { SigningCosmWasmClientOptions } from '@cosmjs/cosmwasm-stargate' import { SignerOptions } from '@cosmos-kit/core' import { Registry } from '@cosmjs/proto-signing/build/registry' import { cosmos, cosmosAminoConverters, cosmwasm, cosmwasmAminoConverters } from 'interchain-query' const GAS_PRICE = new Map([ ['juno', `0.025ujuno`], ['neutron', `0.025untn`], ['terra2', `0.025uluna`], ]) export const getCustomSigningOptions = () => { // registry const registry = new Registry(defaultRegistryTypes) // aminotypes [IMPORTANT: issue happens only when these are not supplied to the options. If they are, they will lead to a signature verification issue. const AMINO_TYPES = new AminoTypes({ ...cosmosAminoConverters, ...cosmwasmAminoConverters, }) cosmos.authz.v1beta1.load(registry) cosmos.staking.v1beta1.load(registry) cosmwasm.wasm.v1.load(registry) const signerOptions: SignerOptions = { signingCosmwasm: ({ chain_name }): SigningCosmWasmClientOptions | undefined => { return { registry, gasPrice: GasPrice.fromString(GAS_PRICE.get(chain_name) || '0.025ujuno'), // aminoTypes: AMINO_TYPES, } }, } return signerOptions } /// useStake.ts import { shiftDigits } from '@/helpers/math' import { MsgExecuteContractEncodeObject } from '@cosmjs/cosmwasm-stargate' import { cosmos } from 'interchain-query' import { useMemo } from 'react' import useSimulate from './useSimulate' import useTransaction from './useTransaction' import useWallet from './useWallet' import { buildStakingGrantMsg } from '@/services/grants' import { Validator } from '@/services/stakingInfoForValidators' import { useBaseCoin } from '@/helpers/chain' import { invalidateUserValidatorQueries } from './useValidators' const { delegate } = cosmos.staking.v1beta1.MessageComposer.fromPartial type StakeParams = { denom: string | undefined amount: string validator: Validator | null expiryDate?: string } const useStake = ({ amount, validator, denom, expiryDate }: StakeParams) => { const { address } = useWallet() const baseCoin = useBaseCoin() const onSuccess = () => { invalidateUserValidatorQueries(baseCoin?.base, address) } const msgs = useMemo(() => { const { decimals } = baseCoin || {} if (!validator || !amount || !denom || !address || !decimals) return const _msgs = [] const stakingMsg = delegate({ delegatorAddress: address, validatorAddress: validator.address, amount: { amount: shiftDigits(amount, decimals), denom, }, }) _msgs.push(stakingMsg) if (validator?.restake?.address) { const grantMsg = buildStakingGrantMsg({ granter: address, grantee: { address: validator.address, botAddress: validator.restake.address, }, maxTokens: { amount: shiftDigits(amount, decimals), denom, }, expiryDate, }) _msgs.push(grantMsg) } return _msgs as MsgExecuteContractEncodeObject[] }, [baseCoin, validator, amount, denom, address, expiryDate]) console.log({ msgs, amount, }) const simulate = useSimulate({ msgs, amount, }) const tx = useTransaction({ msgs, onSuccess, }) ```
j0nl1 commented 11 months ago

@MbBrainz I think this issue is not related with CosmJS. Keplr internally have their own registry they may not have support for that protobuf, I can't say. They decode the message in order to show the message in the UI.

Direct256h1Wallet should be compatible with your code, I can't see the whole code but I would say is what I mention above.

MbBrainz commented 11 months ago

@j0nl1 The error is not specific to keplr, with leap wallet i experience the exact same problem. It happens only to the transaction mentioned above, not to any other type. I tried to remove all the dependencies on other libraries, so that I would be sure its related to comsjs. The code block shows all the imports and constructions.

FYI: You can see the whole code by clicking on the ▶︎ Complete code blocks dropdown