diff --git a/src/components/AboutLedger.js b/src/components/AboutLedger.js index e07eef18..67e72f23 100644 --- a/src/components/AboutLedger.js +++ b/src/components/AboutLedger.js @@ -13,7 +13,7 @@ function AboutLedger(props) { const { daemon_name, chain_id, node_home, codebase } = network.chain.data const { git_repo, binaries } = codebase || {} - const address = validator?.restake.address || GrantAddress + const address = validator?.restake?.address || GrantAddress const show = props.show || (aboutParam == 'ledger' && network.authzSupport) diff --git a/src/components/Delegations.js b/src/components/Delegations.js index 15741e32..9caa655f 100644 --- a/src/components/Delegations.js +++ b/src/components/Delegations.js @@ -35,8 +35,8 @@ class Delegations extends React.Component { } async componentDidMount() { - const isNanoLedger = this.props.wallet?.getIsNanoLedger(); - this.setState({ isNanoLedger: isNanoLedger }); + const ledgerAuthzSupport = this.props.wallet?.ledgerAuthzSupport(); + this.setState({ ledgerAuthzSupport }); this.refresh(true); if (this.props.validator) { @@ -52,9 +52,9 @@ class Delegations extends React.Component { if ((this.props.network !== prevProps.network && !this.props.address) || (this.props.address !== prevProps.address)) { this.clearRefreshInterval() - const isNanoLedger = this.props.wallet?.getIsNanoLedger(); + const ledgerAuthzSupport = this.props.wallet?.ledgerAuthzSupport(); this.setState({ - isNanoLedger: isNanoLedger, + ledgerAuthzSupport: ledgerAuthzSupport, delegations: undefined, rewards: undefined, commission: {}, @@ -297,7 +297,7 @@ class Delegations extends React.Component { } restakePossible() { - return this.props.address && !this.state.isNanoLedger && this.authzSupport(); + return this.props.address && this.state.ledgerAuthzSupport && this.authzSupport(); } totalRewards(validators) { @@ -413,7 +413,7 @@ class Delegations extends React.Component { )} {this.authzSupport() && this.props.operators.length > 0 && - this.state.isNanoLedger && ( + !this.state.ledgerAuthzSupport && ( <> - {address && !isNanoLedger ? 'New Grant' : 'CLI/Ledger instructions'} + {address && !!ledgerAuthzSupport ? 'New Grant' : 'CLI/Ledger instructions'} @@ -156,7 +156,7 @@ function GrantModal(props) { {error} } - {!address || isNanoLedger && ( + {!address || !ledgerAuthzSupport && ( <>

Enter your grant details to generate the relevant CLI command.

@@ -214,7 +214,7 @@ function GrantModal(props) { )}

Incorrect use of Authz grants can be as dangerous as giving away your mnemonic. Make sure you trust the Grantee address and understand the permissions you are granting.

- {address && !isNanoLedger && ( + {address && !!ledgerAuthzSupport && (

diff --git a/src/components/RevokeGrant.js b/src/components/RevokeGrant.js index 5cec83d7..61626e72 100644 --- a/src/components/RevokeGrant.js +++ b/src/components/RevokeGrant.js @@ -51,7 +51,7 @@ function RevokeGrant(props) { } function disabled(){ - return props.disabled || !wallet?.hasPermission(address, 'Revoke') || wallet?.getIsNanoLedger() + return props.disabled || !wallet?.hasPermission(address, 'Revoke') || !wallet?.ledgerAuthzSupport() } if(props.button){ diff --git a/src/converters/authz.mjs b/src/converters/authz.mjs new file mode 100644 index 00000000..05444d74 --- /dev/null +++ b/src/converters/authz.mjs @@ -0,0 +1,126 @@ +import moment from 'moment' +import { GenericAuthorization } from "cosmjs-types/cosmos/authz/v1beta1/authz"; +import { StakeAuthorization } from "cosmjs-types/cosmos/staking/v1beta1/authz"; + +function createAuthzAuthorizationAminoConverter(){ + return { + "/cosmos.authz.v1beta1.GenericAuthorization": { + aminoType: "cosmos-sdk/GenericAuthorization", + toAmino: (value) => GenericAuthorization.decode(value), + fromAmino: ({ msg }) => (GenericAuthorization.encode(GenericAuthorization.fromPartial({ + msg + })).finish()) + }, + "/cosmos.staking.v1beta1.StakeAuthorization": { + aminoType: "cosmos-sdk/StakeAuthorization", + toAmino: (value) => { + const { allowList, maxTokens, authorizationType } = StakeAuthorization.decode(value) + return { + Validators: { + type: "cosmos-sdk/StakeAuthorization/AllowList", + value: { + allow_list: allowList + } + }, + max_tokens: maxTokens, + authorization_type: authorizationType + } + }, + fromAmino: ({ allow_list, max_tokens, authorization_type }) => (StakeAuthorization.encode(StakeAuthorization.fromPartial({ + allowList: allow_list, + maxTokens: max_tokens, + authorizationType: authorization_type + })).finish()) + } + } +} + +const dateConverter = { + toAmino(date){ + return moment(date.seconds.toNumber() * 1000).utc().format() + }, + fromAmino(date){ + return { + seconds: moment(date).unix(), + nanos: 0 + } + } +} + +export function createAuthzAminoConverters() { + const grantConverter = createAuthzAuthorizationAminoConverter() + return { + "/cosmos.authz.v1beta1.MsgGrant": { + aminoType: "cosmos-sdk/MsgGrant", + toAmino: ({ granter, grantee, grant }) => { + converter = grantConverter[grant.authorization.typeUrl] + return { + granter, + grantee, + grant: { + authorization: { + type: converter.aminoType, + value: converter.toAmino(grant.authorization.value) + }, + expiration: dateConverter.toAmino(grant.expiration) + } + } + }, + fromAmino: ({ granter, grantee, grant }) => { + protoType = Object.keys(grantConverter).find(type => grantConverter[type].aminoType === grant.authorization.type) + converter = grantConverter[protoType] + return { + granter, + grantee, + grant: { + authorization: { + typeUrl: protoType, + value: converter.fromAmino(grant.authorization.value) + }, + expiration: dateConverter.fromAmino(grant.expiration) + } + } + }, + }, + "/cosmos.authz.v1beta1.MsgRevoke": { + aminoType: "cosmos-sdk/MsgRevoke", + toAmino: ({ granter, grantee, msgTypeUrl }) => ({ + granter, + grantee, + msg_type_url: msgTypeUrl + }), + fromAmino: ({ granter, grantee, msg_type_url }) => ({ + granter, + grantee, + msgTypeUrl: msg_type_url + }), + }, + }; +} + +export function createAuthzExecAminoConverters(registry, aminoTypes) { + return { + "/cosmos.authz.v1beta1.MsgExec": { + aminoType: "cosmos-sdk/MsgExec", + toAmino: ({ grantee, msgs }) => ({ + grantee, + msgs: msgs.map(({typeUrl, value}) => { + const msgType = registry.lookupType(typeUrl) + return aminoTypes.toAmino({ typeUrl, value: msgType.decode(value) }) + }) + }), + fromAmino: ({ grantee, msgs }) => ({ + grantee, + msgs: msgs.map(({type, value}) => { + const proto = aminoTypes.fromAmino({ type, value }) + const typeUrl = proto.typeUrl + const msgType = registry.lookupType(typeUrl) + return { + typeUrl, + value: msgType.encode(msgType.fromPartial(proto.value)).finish() + } + }) + }), + }, + }; +} diff --git a/src/networks.json b/src/networks.json index a726ca59..653c3b7c 100644 --- a/src/networks.json +++ b/src/networks.json @@ -232,5 +232,9 @@ "average": 0.025, "high": 0.04 } + }, + { + "name": "pulsar", + "testnet": true } ] diff --git a/src/utils/Chain.mjs b/src/utils/Chain.mjs index ff6c69be..23164b9b 100644 --- a/src/utils/Chain.mjs +++ b/src/utils/Chain.mjs @@ -8,9 +8,10 @@ const Chain = (data) => { prettyName: data.prettyName || data.pretty_name, chainId: data.chainId || data.chain_id, prefix: data.prefix || data.bech32_prefix, - slip44: data.slip44 || data.slip44 || 118, + slip44: data.slip44 || 118, estimatedApr: data.params?.calculated_apr, authzSupport: data.authzSupport ?? data.params?.authz, + ledgerAuthzSupport: data.ledgerAuthzSupport ?? false, denom: data.denom || baseAsset?.base?.denom, display: data.display || baseAsset?.display?.denom, symbol: data.symbol || baseAsset?.symbol, diff --git a/src/utils/Network.mjs b/src/utils/Network.mjs index 30fc4a5a..b9236d36 100644 --- a/src/utils/Network.mjs +++ b/src/utils/Network.mjs @@ -86,6 +86,7 @@ class Network { this.estimatedApr = this.chain.estimatedApr this.apyEnabled = data.apyEnabled !== false && !!this.estimatedApr && this.estimatedApr > 0 this.authzSupport = this.chain.authzSupport + this.ledgerAuthzSupport = this.chain.ledgerAuthzSupport this.defaultGasPrice = this.decimals && format(bignumber(multiply(0.000000025, pow(10, this.decimals))), { notation: 'fixed', precision: 4}) + this.denom this.gasPrice = this.data.gasPrice || this.defaultGasPrice if(this.gasPrice){ diff --git a/src/utils/SigningClient.mjs b/src/utils/SigningClient.mjs index a589ba57..d58922b1 100644 --- a/src/utils/SigningClient.mjs +++ b/src/utils/SigningClient.mjs @@ -8,7 +8,6 @@ import { assertIsDeliverTxSuccess, GasPrice, AminoTypes, - createAuthzAminoConverters, createBankAminoConverters, createDistributionAminoConverters, createFreegrantAminoConverters, @@ -25,6 +24,7 @@ import { SignMode } from "cosmjs-types/cosmos/tx/signing/v1beta1/signing.js"; import { AuthInfo, Fee, TxBody, TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx.js"; import { coin } from './Helpers.mjs' +import { createAuthzAminoConverters, createAuthzExecAminoConverters } from '../converters/Authz.mjs' function SigningClient(network, signer) { @@ -32,7 +32,7 @@ function SigningClient(network, signer) { const { restUrl, gasModifier: defaultGasModifier, slip44: coinType, chainId } = network const registry = new Registry(defaultStargateTypes); - const aminoTypes = new AminoTypes({ + const defaultConverters = { ...createAuthzAminoConverters(), ...createBankAminoConverters(), ...createDistributionAminoConverters(), @@ -40,7 +40,9 @@ function SigningClient(network, signer) { ...createStakingAminoConverters(network.prefix), ...createIbcAminoConverters(), ...createFreegrantAminoConverters(), - }) + } + let aminoTypes = new AminoTypes(defaultConverters) + aminoTypes = new AminoTypes({...defaultConverters, ...createAuthzExecAminoConverters(registry, aminoTypes)}) function getAccount(address) { return axios @@ -99,8 +101,7 @@ function SigningClient(network, signer) { const amount = ceil(bignumber(multiply(bignumber(gasPriceAmount.toString()), bignumber(gasLimit.toString())))); return { amount: [coin(amount, denom)], - gas: gasLimit.toString(), - gasLimit: gasLimit.toString() + gas: gasLimit.toString() }; } @@ -184,8 +185,8 @@ function SigningClient(network, signer) { const txBodyBytes = makeBodyBytes(messages, memo) let aminoMsgs try { - aminoMsgs = messages.map(el => aminoTypes.toAmino(el)) - } catch { } + aminoMsgs = convertToAmino(messages) + } catch (e) { console.log(e) } if(aminoMsgs && signer.signAmino){ // Sign as amino if possible for Ledger and Keplr support const signDoc = makeAminoSignDoc(aminoMsgs, fee, chainId, memo, accountNumber, sequence); @@ -237,6 +238,15 @@ function SigningClient(network, signer) { } } + function convertToAmino(messages){ + return messages.map(message => { + if(message.typeUrl.startsWith('/cosmos.authz') && !network.ledgerAuthzSupport){ + throw new Error('This chain does not support amino conversion for Authz messages') + } + return aminoTypes.toAmino(message) + }) + } + function parseTxResult(result){ return { code: result.code, diff --git a/src/utils/Wallet.mjs b/src/utils/Wallet.mjs index 8fd9fed5..48d23353 100644 --- a/src/utils/Wallet.mjs +++ b/src/utils/Wallet.mjs @@ -30,7 +30,7 @@ class Wallet { hasPermission(address, action){ if(address === this.address) return true - if(this.getIsNanoLedger()) return false // Ledger Authz disabled for now + if(!this.ledgerAuthzSupport()) return false let message = messageTypes.find(el => { return el.split('.').slice(-1)[0].replace('Msg', '') === action @@ -43,6 +43,12 @@ class Wallet { }) } + ledgerAuthzSupport(){ + if(!this.getIsNanoLedger()) return true + + return this.network.ledgerAuthzSupport + } + async getAddress(){ this.address = this.address || await this.getAccountAddress()