diff --git a/libs/chains/package.json b/libs/chains/package.json index 580f74bcb0f..cbc5e40425b 100644 --- a/libs/chains/package.json +++ b/libs/chains/package.json @@ -31,6 +31,7 @@ "protobufjs": "^6.1.13" }, "devDependencies": { + "@atomone/atomone-types-long": "^1.0.2", "tsx": "^4.7.2" } } diff --git a/libs/chains/src/index.ts b/libs/chains/src/index.ts index 760d4e7e524..f60cde6f5cb 100644 --- a/libs/chains/src/index.ts +++ b/libs/chains/src/index.ts @@ -17,6 +17,9 @@ export { type QueryVotesResponseSDKType, } from './cosmos-ts/src/codegen/cosmos/gov/v1/query'; export { LCDQueryClient as GovV1Client } from './cosmos-ts/src/codegen/cosmos/gov/v1/query.lcd'; + +export { LCDQueryClient as GovV1AtomOneClient } from '@atomone/atomone-types-long/atomone/gov/v1/query.lcd'; +export { createLCDClient as createAtomOneLCDClient } from '@atomone/atomone-types-long/atomone/lcd'; export { createLCDClient } from './cosmos-ts/src/codegen/cosmos/lcd'; export * from './cosmos-ts/src/codegen/google/protobuf/any'; export * from './cosmos-ts/src/codegen/helpers'; diff --git a/libs/shared/src/types/protocol.ts b/libs/shared/src/types/protocol.ts index 84239a5011a..c5e1a156fc7 100644 --- a/libs/shared/src/types/protocol.ts +++ b/libs/shared/src/types/protocol.ts @@ -129,6 +129,7 @@ export enum ChainNetwork { */ export enum CosmosGovernanceVersion { v1 = 'v1', + v1atomone = 'v1atomone', v1beta1govgen = 'v1beta1govgen', v1beta1 = 'v1beta1', v1beta1Failed = 'v1beta1-attempt-failed', diff --git a/packages/commonwealth/client/scripts/controllers/chain/cosmos/adapter.ts b/packages/commonwealth/client/scripts/controllers/chain/cosmos/adapter.ts index 178b12c322d..ff57d499760 100644 --- a/packages/commonwealth/client/scripts/controllers/chain/cosmos/adapter.ts +++ b/packages/commonwealth/client/scripts/controllers/chain/cosmos/adapter.ts @@ -6,6 +6,7 @@ import IChainAdapter from '../../../models/IChainAdapter'; import type CosmosAccount from './account'; import CosmosAccounts from './accounts'; import CosmosChain from './chain'; +import CosmosGovernanceV1AtomOne from './gov/atomone/governance-v1'; import CosmosGovernanceGovgen from './gov/govgen/governance-v1beta1'; import CosmosGovernanceV1 from './gov/v1/governance-v1'; import CosmosGovernance from './gov/v1beta1/governance-v1beta1'; @@ -17,7 +18,8 @@ class Cosmos extends IChainAdapter { public governance: | CosmosGovernance | CosmosGovernanceV1 - | CosmosGovernanceGovgen; + | CosmosGovernanceGovgen + | CosmosGovernanceV1AtomOne; public readonly base = ChainBase.CosmosSDK; @@ -28,9 +30,11 @@ class Cosmos extends IChainAdapter { this.governance = meta?.ChainNode?.cosmos_gov_version === 'v1beta1govgen' ? new CosmosGovernanceGovgen(this.app) - : meta?.ChainNode?.cosmos_gov_version === 'v1' - ? new CosmosGovernanceV1(this.app) - : new CosmosGovernance(this.app); + : meta?.ChainNode?.cosmos_gov_version === 'v1atomone' + ? new CosmosGovernanceV1AtomOne(this.app) + : meta?.ChainNode?.cosmos_gov_version === 'v1' + ? new CosmosGovernanceV1(this.app) + : new CosmosGovernance(this.app); } public async initApi() { diff --git a/packages/commonwealth/client/scripts/controllers/chain/cosmos/chain.ts b/packages/commonwealth/client/scripts/controllers/chain/cosmos/chain.ts index 648ed63a131..38e293d9457 100644 --- a/packages/commonwealth/client/scripts/controllers/chain/cosmos/chain.ts +++ b/packages/commonwealth/client/scripts/controllers/chain/cosmos/chain.ts @@ -17,7 +17,7 @@ import { import BN from 'bn.js'; import { CosmosToken } from 'controllers/chain/cosmos/types'; import moment from 'moment'; -import { LCD } from 'shared/chain/types/cosmos'; +import { AtomOneLCD, LCD } from 'shared/chain/types/cosmos'; import type { IApp } from 'state'; import { ApiStatus } from 'state'; import { SERVER_URL } from 'state/api/config'; @@ -36,12 +36,14 @@ import { getCosmosChains } from '../../app/webWallets/utils'; import WebWalletController from '../../app/web_wallets'; import type CosmosAccount from './account'; import { + getAtomOneLCDClient, getLCDClient, getRPCClient, getSigningClient, getTMClient, } from './chain.utils'; import EthSigningClient from './eth_signing_client'; +import type { AtomOneGovExtension } from './gov/atomone/queries-v1'; import type { GovgenGovExtension } from './gov/govgen/queries-v1beta1'; /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -59,11 +61,12 @@ export type CosmosApiType = QueryClient & StakingExtension & GovExtension & GovgenGovExtension & + AtomOneGovExtension & BankExtension; class CosmosChain implements IChainModule { private _api: CosmosApiType; - private _lcd: LCD; + private _lcd: LCD | AtomOneLCD; public get api() { return this._api; @@ -136,7 +139,18 @@ class CosmosChain implements IChainModule { console.error('Error starting LCD client: ', e); } } - + if ( + chain?.ChainNode?.cosmos_gov_version === CosmosGovernanceVersion.v1atomone + ) { + try { + const lcdUrl = `${window.location.origin}${SERVER_URL}/cosmosProxy/v1/${chain.id}`; + console.log(`Starting LCD API at ${lcdUrl}...`); + const lcd = await getAtomOneLCDClient(lcdUrl); + this._lcd = lcd; + } catch (e) { + console.error('Error starting LCD client: ', e); + } + } await this.fetchBlock(); // Poll for new block immediately } diff --git a/packages/commonwealth/client/scripts/controllers/chain/cosmos/chain.utils.ts b/packages/commonwealth/client/scripts/controllers/chain/cosmos/chain.utils.ts index 837cbb47a3f..155698ae3d9 100644 --- a/packages/commonwealth/client/scripts/controllers/chain/cosmos/chain.utils.ts +++ b/packages/commonwealth/client/scripts/controllers/chain/cosmos/chain.utils.ts @@ -5,12 +5,14 @@ import { createDefaultAminoConverters, } from '@cosmjs/stargate'; import { Tendermint34Client } from '@cosmjs/tendermint-rpc'; -import { LCD } from '../../../../../shared/chain/types/cosmos'; +import { AtomOneLCD, LCD } from '../../../../../shared/chain/types/cosmos'; import { CosmosApiType } from './chain'; import { createAltGovAminoConverters, + createAtomoneGovAminoConverters, createGovgenGovAminoConverters, } from './gov/aminomessages'; +import { setupAtomOneExtension } from './gov/atomone/queries-v1'; import { setupGovgenExtension } from './gov/govgen/queries-v1beta1'; export const getTMClient = async ( @@ -29,6 +31,7 @@ export const getRPCClient = async ( cosm.setupGovExtension, cosm.setupStakingExtension, setupGovgenExtension, + setupAtomOneExtension, cosm.setupBankExtension, ); return client; @@ -42,6 +45,15 @@ export const getLCDClient = async (lcdUrl: string): Promise => { }); }; +export const getAtomOneLCDClient = async ( + lcdUrl: string, +): Promise => { + const { createAtomOneLCDClient } = await import('@hicommonwealth/chains'); + + return await createAtomOneLCDClient({ + restEndpoint: lcdUrl, + }); +}; export const getSigningClient = async ( url: string, signer: OfflineSigner, @@ -50,6 +62,7 @@ export const getSigningClient = async ( ...createDefaultAminoConverters(), ...createAltGovAminoConverters(), ...createGovgenGovAminoConverters(), + ...createAtomoneGovAminoConverters(), }); return await SigningStargateClient.connectWithSigner(url, signer, { diff --git a/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/aminomessages.ts b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/aminomessages.ts index 1eba95517d2..1fb4ed2609a 100644 --- a/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/aminomessages.ts +++ b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/aminomessages.ts @@ -1,3 +1,4 @@ +import { AminoConverter as AtomOneAminoConverter } from '@atomone/atomone-types-long/atomone/gov/v1beta1/tx.amino'; import { AminoConverter } from '@atomone/govgen-types-long/govgen/gov/v1beta1/tx.amino'; import { AminoMsg } from '@cosmjs/amino'; import { AminoMsgSubmitProposal } from '@cosmjs/stargate'; @@ -17,6 +18,9 @@ export function isAminoMsgSubmitProposal( return msg.type === 'cosmos-sdk/MsgSubmitProposal'; } export function createGovgenGovAminoConverters(): AminoConverters { + return AtomOneAminoConverter; +} +export function createAtomoneGovAminoConverters(): AminoConverters { return AminoConverter; } export function createAltGovAminoConverters(): AminoConverters { diff --git a/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/governance-v1.ts b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/governance-v1.ts new file mode 100644 index 00000000000..06cd78f3824 --- /dev/null +++ b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/governance-v1.ts @@ -0,0 +1,121 @@ +import { Any, numberToLong } from '@hicommonwealth/chains'; +import type { + CosmosToken, + ICosmosProposal, +} from 'controllers/chain/cosmos/types'; +import ProposalModule from 'models/ProposalModule'; +import { ITXModalData } from 'models/interfaces'; +import type CosmosAccount from '../../account'; +import type CosmosAccounts from '../../accounts'; +import type CosmosChain from '../../chain'; +import type { CosmosApiType } from '../../chain'; +import { CosmosProposalV1AtomOne } from './proposal-v1'; +import { encodeMsgSubmitProposal, propToIProposal } from './utils-v1'; + +/** This file is a copy of controllers/chain/cosmos/governance.ts, modified for + * gov module version v1. This is considered a patch to make sure v1-enabled chains + * load proposals. Eventually we will ideally move back to one governance.ts file. + * Patch state: + * + * - governance.ts uses cosmJS v1beta1 gov + * - governance-v1.ts uses telescope-generated v1 gov */ +class CosmosGovernanceV1AtomOne extends ProposalModule< + CosmosApiType, + ICosmosProposal, + // @ts-expect-error StrictNullChecks + CosmosProposalV1AtomOne +> { + private _minDeposit: CosmosToken; + + public get minDeposit() { + return this._minDeposit; + } + + public setMinDeposit(minDeposit: CosmosToken) { + this._minDeposit = minDeposit; + } + + private _Chain: CosmosChain; + private _Accounts: CosmosAccounts; + + /* eslint-disable-next-line @typescript-eslint/require-await */ + public async init( + ChainInfo: CosmosChain, + Accounts: CosmosAccounts, + ): Promise { + this._Chain = ChainInfo; + this._Accounts = Accounts; + this._initialized = true; + } + + public async getProposal( + proposalId: number, + ): Promise { + const existingProposal = this.store.getByIdentifier(proposalId); + if (existingProposal) { + return existingProposal; + } + return this._initProposal(proposalId); + } + + // @ts-expect-error StrictNullChecks + private async _initProposal(proposalId: number): Promise { + try { + // @ts-expect-error StrictNullChecks + if (!proposalId) return; + const { proposal } = await this._Chain.lcd.atomone.gov.v1.proposal({ + proposalId: numberToLong(proposalId), + }); + const cosmosProposal = new CosmosProposalV1AtomOne( + this._Chain, + this._Accounts, + this, + // @ts-expect-error StrictNullChecks + propToIProposal(proposal), + ); + await cosmosProposal.init(); + return cosmosProposal; + } catch (error) { + console.error('Error fetching proposal: ', error); + } + } + + public createTx( + sender: CosmosAccount, + title: string, + description: string, + initialDeposit: CosmosToken, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + memo = '', + ): ITXModalData { + throw new Error('unsupported'); + } + + // TODO: support multiple deposit types + public async submitProposalTx( + sender: CosmosAccount, + initialDeposit: CosmosToken, + content: Any, + ): Promise { + const msg = encodeMsgSubmitProposal( + sender.address, + initialDeposit, + content, + ); + + // fetch completed proposal from returned events + const events = await this._Chain.sendTx(sender, msg); + console.log(events); + const submitEvent = events?.find((e) => e.type === 'submit_proposal'); + const cosm = await import('@cosmjs/encoding'); + const idAttribute = submitEvent?.attributes.find( + ({ key }) => key && cosm.fromAscii(key) === 'proposal_id', + ); + // @ts-expect-error StrictNullChecks + const id = +cosm.fromAscii(idAttribute.value); + await this._initProposal(id); + return id; + } +} + +export default CosmosGovernanceV1AtomOne; diff --git a/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/proposal-v1.ts b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/proposal-v1.ts new file mode 100644 index 00000000000..f72a14cc051 --- /dev/null +++ b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/proposal-v1.ts @@ -0,0 +1,347 @@ +import type { + QueryDepositsResponseSDKType, + QueryTallyResultResponseSDKType, + QueryVotesResponseSDKType, +} from '@atomone/atomone-types-long/atomone/gov/v1/query'; +import { EncodeObject } from '@cosmjs/proto-signing'; +import { longify } from '@cosmjs/stargate/build/queryclient'; +import { ProposalType } from '@hicommonwealth/shared'; +import BN from 'bn.js'; +import type { + CosmosProposalState, + CosmosToken, + CosmosVoteChoice, + ICosmosProposal, +} from 'controllers/chain/cosmos/types'; +import Long from 'long'; +import Proposal from 'models/Proposal'; +import { ITXModalData } from 'models/interfaces'; +import { + ProposalEndTime, + ProposalStatus, + VotingType, + VotingUnit, +} from 'models/types'; +import { DepositVote } from 'models/votes'; +import moment from 'moment'; +import { AtomOneLCD } from 'shared/chain/types/cosmos'; +import CosmosAccount from '../../account'; +import type CosmosAccounts from '../../accounts'; +import type CosmosChain from '../../chain'; +import type { CosmosApiType } from '../../chain'; +import { CosmosVote } from '../v1beta1/proposal-v1beta1'; +import { encodeMsgVote } from '../v1beta1/utils-v1beta1'; +import CosmosGovernanceV1AtomOne from './governance-v1'; +import { marshalTallyV1 } from './utils-v1'; + +const voteToEnumV1 = (voteOption: number | string): CosmosVoteChoice => { + switch (voteOption) { + case 'VOTE_OPTION_YES': + return 'Yes'; + case 'VOTE_OPTION_NO': + return 'No'; + case 'VOTE_OPTION_ABSTAIN': + return 'Abstain'; + case 'VOTE_OPTION_NO_WITH_VETO': + return 'NoWithVeto'; + default: + // @ts-expect-error StrictNullChecks + return null; + } +}; + +export class CosmosProposalV1AtomOne extends Proposal< + CosmosApiType, + CosmosToken, + ICosmosProposal, + CosmosVote +> { + public get shortIdentifier() { + return `#${this.identifier.toString()}`; + } + + public get title(): string { + return this.data.title || this._metadata?.title; + } + + public get description() { + return ( + this.data.description || + this._metadata?.summary || + this._metadata?.description + ); + } + + // @ts-expect-error StrictNullChecks + public get author() { + return this.data.proposer + ? this._Accounts.fromAddress(this.data.proposer) + : null; + } + + public get votingType() { + if (this.status === 'DepositPeriod') { + return VotingType.SimpleYesApprovalVoting; + } + return VotingType.YesNoAbstainVeto; + } + + public get votingUnit() { + return VotingUnit.CoinVote; + } + + public canVoteFrom(account) { + // TODO: balance check + return account instanceof CosmosAccount; + } + + public get status(): CosmosProposalState { + return this.data.state.status; + } + + public get depositorsAsVotes(): Array> { + return this.data.state.depositors.map( + ([a, n]) => + new DepositVote(this._Accounts.fromAddress(a), this._Chain.coins(n)), + ); + } + + private _metadata: any; + public get metadata() { + return this._metadata; + } + + private _Chain: CosmosChain; + private _Accounts: CosmosAccounts; + private _Governance: CosmosGovernanceV1AtomOne; + + constructor( + ChainInfo: CosmosChain, + Accounts: CosmosAccounts, + Governance: CosmosGovernanceV1AtomOne, + data: ICosmosProposal, + ) { + super(ProposalType.CosmosProposal, data); + this._Chain = ChainInfo; + this._Accounts = Accounts; + this._Governance = Governance; + this.createdAt = data.submitTime; + this._Governance.store.add(this); + } + + public update() { + throw new Error('unimplemented'); + } + + public updateMetadata(metadata: any) { + this._metadata = metadata; + if (!this.data.title) { + this.data.title = metadata.title; + } + if (!this.data.description) { + this.data.description = metadata.description || metadata.summary; + } + this._Governance.store.update(this); + } + + public async init() { + if (!this.initialized) { + this._initialized = true; + } + if (this.data.state.completed) { + // @ts-expect-error StrictNullChecks + super.complete(this._Governance.store); + } + } + + public async fetchDeposits(): Promise { + const proposalId = longify(this.data.identifier) as Long; + const deposits = await ( + this._Chain.lcd as AtomOneLCD + ).atomone.gov.v1.deposits({ + proposalId, + }); + this.setDeposits(deposits); + return deposits; + } + + public async fetchTally(): Promise { + const proposalId = longify(this.data.identifier) as Long; + const tally = await ( + this._Chain.lcd as AtomOneLCD + ).atomone.gov.v1.tallyResult({ + proposalId, + }); + this.setTally(tally); + return tally; + } + + public async fetchVotes(): Promise { + const proposalId = longify(this.data.identifier) as Long; + const votes = await (this._Chain.lcd as AtomOneLCD).atomone.gov.v1.votes({ + proposalId, + }); + this.setVotes(votes); + return votes; + } + + public setDeposits(depositResp: QueryDepositsResponseSDKType) { + if (depositResp?.deposits) { + for (const deposit of depositResp.deposits) { + if (deposit.amount && deposit.amount[0]) { + this.data.state.depositors.push([ + deposit.depositor, + new BN(deposit.amount[0].amount), + ]); + } + } + } + } + + public setTally(tallyResp: QueryTallyResultResponseSDKType) { + if (tallyResp?.tally) { + this.data.state.tally = marshalTallyV1(tallyResp?.tally); + } + } + + public setVotes(votesResp: QueryVotesResponseSDKType) { + if (votesResp) { + for (const voter of votesResp.votes) { + const vote = voteToEnumV1(voter.options[0].option); + if (vote) { + this.data.state.voters.push([voter.voter, vote]); + this.addOrUpdateVote( + new CosmosVote(this._Accounts.fromAddress(voter.voter), vote), + ); + } else { + console.error( + `voter: ${voter.voter} has invalid vote option: ${voter.options[0].option}`, + ); + } + } + } + } + + // TODO: add getters for various vote features: tally, quorum, threshold, veto + // see: https://blog.chorus.one/an-overview-of-cosmos-hub-governance/ + get support() { + if (this.status === 'DepositPeriod') { + return this._Chain.coins(this.data.state.totalDeposit); + } + if (!this.data.state.tally) return 0; + const nonAbstainingPower = this.data.state.tally.no + .add(this.data.state.tally.noWithVeto) + .add(this.data.state.tally.yes); + if (nonAbstainingPower.eqn(0)) return 0; + const ratioPpm = this.data.state.tally.yes + .muln(1_000_000) + .div(nonAbstainingPower); + return +ratioPpm / 1_000_000; + } + + get turnout() { + if (this.status === 'DepositPeriod') { + if (this.data.state.totalDeposit.eqn(0) || !this._Chain.staked) { + return 0; + } else { + const ratioInPpm = +this.data.state.totalDeposit + .muln(1_000_000) + .div(this._Chain.staked); + return +ratioInPpm / 1_000_000; + } + } + if (!this.data.state.tally) return 0; + // all voters automatically abstain, so we compute turnout as the percent non-abstaining + const totalVotingPower = this.data.state.tally.no + .add(this.data.state.tally.noWithVeto) + .add(this.data.state.tally.yes) + .add(this.data.state.tally.abstain); + if (totalVotingPower.eqn(0)) return 0; + const ratioInPpm = +this.data.state.tally.abstain + .muln(1_000_000) + .div(totalVotingPower); + return 1 - ratioInPpm / 1_000_000; + } + + get veto() { + if (!this.data.state.tally) return 0; + const totalVotingPower = this.data.state.tally.no + .add(this.data.state.tally.noWithVeto) + .add(this.data.state.tally.yes) + .add(this.data.state.tally.abstain); + if (totalVotingPower.eqn(0)) return 0; + const ratioInPpm = +this.data.state.tally.noWithVeto + .muln(1_000_000) + .div(totalVotingPower); + return ratioInPpm / 1_000_000; + } + + get endTime(): ProposalEndTime { + // if in deposit period: at most create time + maxDepositTime + if (this.status === 'DepositPeriod') { + if (!this.data.depositEndTime) return { kind: 'unavailable' }; + return { kind: 'fixed', time: moment(this.data.depositEndTime) }; + } + // if in voting period: exactly voting start time + votingTime + if (!this.data.votingEndTime) return { kind: 'unavailable' }; + return { kind: 'fixed', time: moment(this.data.votingEndTime) }; + } + + get isPassing(): ProposalStatus { + switch (this.status) { + case 'Passed': + return ProposalStatus.Passed; + case 'Rejected': + return ProposalStatus.Failed; + case 'VotingPeriod': + return +this.support > 0.5 && this.veto <= 1 / 3 + ? ProposalStatus.Passing + : ProposalStatus.Failing; + case 'DepositPeriod': + return this._Governance.minDeposit + ? this.data.state.totalDeposit.gte(this._Governance.minDeposit) + ? ProposalStatus.Passing + : ProposalStatus.Failing + : ProposalStatus.None; + default: + return ProposalStatus.None; + } + } + + // TRANSACTIONS + public async submitDepositTx(depositor: CosmosAccount, amount: CosmosToken) { + if (this.status !== 'DepositPeriod') { + throw new Error('proposal not in deposit period'); + } + const cosm = await import('@cosmjs/stargate/build/queryclient'); + const msg: EncodeObject = { + typeUrl: '/atomone.gov.v1beta1.MsgDeposit', + value: { + proposalId: cosm.longify(this.data.identifier), + depositor: depositor.address, + amount: [amount.toCoinObject()], + }, + }; + await this._Chain.sendTx(depositor, msg); + this.data.state.depositors.push([depositor.address, new BN(+amount)]); + } + + public async voteTx(vote: CosmosVote) { + if (this.status !== 'VotingPeriod') { + throw new Error('proposal not in voting period'); + } + const msg = encodeMsgVote( + vote.account.address, + this.data.identifier, + vote.option, + ); + + await this._Chain.sendTx(vote.account, msg); + this.addOrUpdateVote(vote); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public submitVoteTx(vote: CosmosVote, memo = '', cb?): ITXModalData { + throw new Error('unsupported'); + } +} diff --git a/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/queries-v1.ts b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/queries-v1.ts new file mode 100644 index 00000000000..108ceafa666 --- /dev/null +++ b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/queries-v1.ts @@ -0,0 +1,142 @@ +import { ProposalStatus } from '@atomone/atomone-types-long/atomone/gov/v1beta1/gov'; +import { + QueryClientImpl, + QueryDepositResponse, + QueryDepositsResponse, + QueryParamsResponse, + QueryProposalResponse, + QueryProposalsResponse, + QueryTallyResultResponse, + QueryVoteResponse, + QueryVotesResponse, +} from '@atomone/atomone-types-long/atomone/gov/v1beta1/query'; +import { Uint64 } from '@cosmjs/math'; +import { + QueryClient, + createPagination, + createProtobufRpcClient, + longify, +} from '@cosmjs/stargate/build/queryclient'; +/* +import { + createPagination, + createProtobufRpcClient, + longify, + QueryClient, +} from '../../queryclient'; +*/ +export type GovParamsType = 'deposit' | 'tallying' | 'voting'; + +export type GovProposalId = string | number | Uint64; + +export interface AtomOneGovExtension { + readonly atomone: { + readonly params: ( + parametersType: GovParamsType, + ) => Promise; + readonly proposals: ( + proposalStatus: ProposalStatus, + depositor: string, + voter: string, + paginationKey?: Uint8Array, + ) => Promise; + readonly proposal: ( + proposalId: GovProposalId, + ) => Promise; + readonly deposits: ( + proposalId: GovProposalId, + paginationKey?: Uint8Array, + ) => Promise; + readonly deposit: ( + proposalId: GovProposalId, + depositorAddress: string, + ) => Promise; + readonly tally: ( + proposalId: GovProposalId, + ) => Promise; + readonly votes: ( + proposalId: GovProposalId, + paginationKey?: Uint8Array, + ) => Promise; + readonly vote: ( + proposalId: GovProposalId, + voterAddress: string, + ) => Promise; + }; +} + +export function setupAtomOneExtension(base: QueryClient): AtomOneGovExtension { + 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 { + atomone: { + 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; + }, + }, + }; +} diff --git a/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/utils-v1.ts b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/utils-v1.ts new file mode 100644 index 00000000000..90f4c66346f --- /dev/null +++ b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/atomone/utils-v1.ts @@ -0,0 +1,220 @@ +import { + ProposalSDKType, + ProposalStatus, + TallyResultSDKType, +} from '@atomone/atomone-types-long/atomone/gov/v1/gov'; +import BN from 'bn.js'; +import moment from 'moment'; +import type { AtomOneLCD } from '../../../../../../../shared/chain/types/cosmos'; +import type { + CosmosProposalState, + ICosmosProposal, + ICosmosProposalTally, +} from '../../types'; + +import { EncodeObject } from '@cosmjs/proto-signing'; +import { CosmosToken } from 'controllers/chain/cosmos/types'; +import { Any } from 'cosmjs-types/google/protobuf/any'; +import { isCompleted } from '../v1beta1/utils-v1beta1'; + +/* Governance helper methods for Cosmos chains with gov module v1 (as of Cosmos SDK v0.46.11) */ + +export const fetchProposalsByStatusV1AtomOne = async ( + lcd: AtomOneLCD, + status: ProposalStatus, +): Promise => { + try { + const { proposals: proposalsByStatus, pagination } = + await lcd.atomone.gov.v1.proposals({ + proposalStatus: status, + voter: '', + depositor: '', + }); + + let nextKey = pagination?.next_key; + + // @ts-expect-error StrictNullChecks + while (nextKey?.length > 0) { + // TODO: temp fix to handle chains that return nextKey as a string instead of Uint8Array + // Our v1 API needs to handle this better. To be addressed in #6610 + if (typeof nextKey === 'string') { + nextKey = new Uint8Array(Buffer.from(nextKey, 'base64')); + } + + const { proposals, pagination: nextPage } = + await lcd.atomone.gov.v1.proposals({ + proposalStatus: status, + voter: '', + depositor: '', + pagination: { + // @ts-expect-error StrictNullChecks + key: nextKey, + // @ts-expect-error StrictNullChecks + limit: undefined, + // @ts-expect-error StrictNullChecks + offset: undefined, + countTotal: true, + reverse: true, + }, + }); + proposalsByStatus.push(...proposals); + // @ts-expect-error StrictNullChecks + nextKey = nextPage.next_key; + } + return proposalsByStatus; + } catch (e) { + console.error(`Error fetching proposal by status ${status}`, e); + return []; + } +}; + +export const getActiveProposalsV1AtomOne = async ( + lcd: AtomOneLCD, +): Promise => { + const votingPeriodProposals = await fetchProposalsByStatusV1AtomOne( + lcd, + ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD, + ); + const depositPeriodProposals = await fetchProposalsByStatusV1AtomOne( + lcd, + ProposalStatus.PROPOSAL_STATUS_DEPOSIT_PERIOD, + ); + return sortProposalsV1AtomOne([ + ...votingPeriodProposals, + ...depositPeriodProposals, + ]); +}; + +export const getCompletedProposalsV1AtomOne = async ( + lcd: AtomOneLCD, +): Promise => { + const passedProposals = await fetchProposalsByStatusV1AtomOne( + lcd, + ProposalStatus.PROPOSAL_STATUS_PASSED, + ); + const failedProposals = await fetchProposalsByStatusV1AtomOne( + lcd, + ProposalStatus.PROPOSAL_STATUS_FAILED, + ); + const rejectedProposals = await fetchProposalsByStatusV1AtomOne( + lcd, + ProposalStatus.PROPOSAL_STATUS_REJECTED, + ); + const combined = [ + ...passedProposals, + ...failedProposals, + ...rejectedProposals, + ]; + return sortProposalsV1AtomOne(combined); +}; + +export const sortProposalsV1AtomOne = ( + proposals: ProposalSDKType[], +): ICosmosProposal[] => { + return proposals + .map((p) => propToIProposal(p)) + .filter((p): p is ICosmosProposal => !!p) + .sort((p1, p2) => +p2!.identifier - +p1!.identifier); +}; + +export const propToIProposal = (p: ProposalSDKType): ICosmosProposal | null => { + const status = stateEnumToStringV1(p.status.toString()); + const identifier = p.id.toString(); + let title = ''; + let description = ''; + let messages = []; + if (p.messages?.length > 0) { + // @ts-expect-error StrictNullChecks + messages = p.messages.map((m) => { + const content = m['content']; + // get title and description from 1st message if no top-level title/desc + if (!title) title = content?.title; + if (!description) description = content?.description; + return m; + }); + } + + return { + identifier, + type: 'text', + title, + description, + messages, + metadata: p.metadata, + // @ts-expect-error StrictNullChecks + submitTime: moment.unix(new Date(p.submit_time).valueOf() / 1000), + // @ts-expect-error StrictNullChecks + depositEndTime: moment.unix(new Date(p.deposit_end_time).valueOf() / 1000), + // @ts-expect-error StrictNullChecks + votingEndTime: moment.unix(new Date(p.voting_end_time).valueOf() / 1000), + votingStartTime: moment.unix( + // @ts-expect-error StrictNullChecks + new Date(p.voting_start_time).valueOf() / 1000, + ), + // @ts-expect-error StrictNullChecks + proposer: null, + state: { + identifier, + completed: isCompleted(status), + status, + // TODO: handle non-default amount + totalDeposit: + p.total_deposit && p.total_deposit[0] + ? new BN(p.total_deposit[0].amount) + : new BN(0), + depositors: [], + voters: [], + // @ts-expect-error StrictNullChecks + tally: p.final_tally_result && marshalTallyV1(p.final_tally_result), + }, + }; +}; + +const stateEnumToStringV1 = (status: string): CosmosProposalState => { + switch (status) { + case 'PROPOSAL_STATUS_UNSPECIFIED': + return 'Unspecified'; + case 'PROPOSAL_STATUS_DEPOSIT_PERIOD': + return 'DepositPeriod'; + case 'PROPOSAL_STATUS_VOTING_PERIOD': + return 'VotingPeriod'; + case 'PROPOSAL_STATUS_PASSED': + return 'Passed'; + case 'PROPOSAL_STATUS_FAILED': + return 'Failed'; + case 'PROPOSAL_STATUS_REJECTED': + return 'Rejected'; + case 'UNRECOGNIZED': + return 'Unrecognized'; + default: + throw new Error(`Invalid proposal state: ${status}`); + } +}; + +export const marshalTallyV1 = ( + tally: TallyResultSDKType, +): ICosmosProposalTally => { + // @ts-expect-error StrictNullChecks + if (!tally) return null; + return { + yes: new BN(tally.yes_count), + abstain: new BN(tally.abstain_count), + no: new BN(tally.no_count), + noWithVeto: new BN(0), + }; +}; + +export const encodeMsgSubmitProposalAtomOne = ( + sender: string, + initialDeposit: CosmosToken, + content: Any, +): EncodeObject => { + return { + typeUrl: '/atomone.gov.v1beta1.MsgSubmitProposal', + value: { + initialDeposit: [initialDeposit.toCoinObject()], + proposer: sender, + content, + }, + }; +}; diff --git a/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/utils.ts b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/utils.ts index 56f2a933227..c3bc5c49342 100644 --- a/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/utils.ts +++ b/packages/commonwealth/client/scripts/controllers/chain/cosmos/gov/utils.ts @@ -1,5 +1,8 @@ import { CosmosGovernanceVersion } from '@hicommonwealth/shared'; import Cosmos from '../adapter'; +import CosmosGovernanceV1AtomOne from './atomone/governance-v1'; +import { CosmosProposalV1AtomOne } from './atomone/proposal-v1'; +import { getCompletedProposalsV1AtomOne } from './atomone/utils-v1'; import CosmosGovernanceGovgen from './govgen/governance-v1beta1'; import { CosmosProposalGovgen } from './govgen/proposal-v1beta1'; import { @@ -24,6 +27,8 @@ export const getCompletedProposals = async ( ): Promise => { const { chain, accounts, governance, meta } = cosmosChain; console.log(cosmosChain); + const isAtomone = + meta.ChainNode?.cosmos_gov_version === CosmosGovernanceVersion.v1atomone; const isGovgen = meta.ChainNode?.cosmos_gov_version === CosmosGovernanceVersion.v1beta1govgen; @@ -34,7 +39,19 @@ export const getCompletedProposals = async ( CosmosGovernanceVersion.v1beta1Failed; let cosmosProposals = []; - if (isGovgen) { + if (isAtomone) { + const v1proposals = await getCompletedProposalsV1AtomOne(chain.api); + // @ts-expect-error StrictNullChecks + cosmosProposals = v1proposals.map( + (p) => + new CosmosProposalV1AtomOne( + chain, + accounts, + governance as CosmosGovernanceV1AtomOne, + p, + ), + ); + } else if (isGovgen) { const v1Beta1Proposals = await getCompletedProposalsGovgen(chain.api); // @ts-expect-error StrictNullChecks cosmosProposals = v1Beta1Proposals.map( diff --git a/packages/commonwealth/package.json b/packages/commonwealth/package.json index e579e10fc4b..9ca94aa696a 100644 --- a/packages/commonwealth/package.json +++ b/packages/commonwealth/package.json @@ -109,12 +109,12 @@ "@hicommonwealth/adapters": "workspace:*", "@hicommonwealth/chains": "workspace:*", "@hicommonwealth/core": "workspace:*", + "@hicommonwealth/evm-protocols": "workspace:*", "@hicommonwealth/evm-testing": "workspace:*", "@hicommonwealth/model": "workspace:*", "@hicommonwealth/schemas": "workspace:*", "@hicommonwealth/shared": "workspace:*", "@hicommonwealth/sitemaps": "workspace:*", - "@hicommonwealth/evm-protocols": "workspace:*", "@hookform/resolvers": "^3.3.1", "@ipld/dag-json": "^10.2.0", "@keplr-wallet/types": "^0.12.23", @@ -285,6 +285,7 @@ "zustand": "^4.3.8" }, "devDependencies": { + "@atomone/atomone-types-long": "^1.0.2", "@ethersproject/keccak256": "5.7.0", "@types/express": "^4.17.21", "@types/passport": "^1.0.16", diff --git a/packages/commonwealth/shared/chain/types/cosmos.ts b/packages/commonwealth/shared/chain/types/cosmos.ts index cf1caf3c8c5..b5d0b8704f8 100644 --- a/packages/commonwealth/shared/chain/types/cosmos.ts +++ b/packages/commonwealth/shared/chain/types/cosmos.ts @@ -1,4 +1,4 @@ -import { GovV1Client } from '@hicommonwealth/chains'; +import { GovV1AtomOneClient, GovV1Client } from '@hicommonwealth/chains'; // currently just used for gov v1, but this can be expanded export type LCD = { @@ -8,3 +8,10 @@ export type LCD = { }; }; }; +export type AtomOneLCD = { + atomone: { + gov: { + v1: GovV1AtomOneClient; + }; + }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ba7ed69a68..9982449e8ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,6 +434,9 @@ importers: specifier: ^6.1.13 version: 6.11.4 devDependencies: + '@atomone/atomone-types-long': + specifier: ^1.0.2 + version: 1.0.2 tsx: specifier: ^4.7.2 version: 4.9.3 @@ -1401,6 +1404,9 @@ importers: specifier: ^4.3.8 version: 4.5.2(@types/react@18.3.3)(react@18.3.1) devDependencies: + '@atomone/atomone-types-long': + specifier: ^1.0.2 + version: 1.0.2 '@ethersproject/keccak256': specifier: 5.7.0 version: 5.7.0 @@ -1502,6 +1508,9 @@ packages: resolution: {integrity: sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==} engines: {node: '>=4'} + '@atomone/atomone-types-long@1.0.2': + resolution: {integrity: sha512-YVX8hyL7ljzNpiaFKA3waTZJzgKLH2SeCL0wjGzaTaFi+4Nb3gwceXfhoqSijRYChEwXtKXb5XPaS2uv88mgFA==} + '@atomone/govgen-types-long@0.3.9': resolution: {integrity: sha512-TcjEuvqWXuOegAqpBbZt1HUX6CePZtrNmj2ZNxxv7AFlpjNVK47pcrqFH9zErwCGnPVL4lxawu7eZRlGO2CRqw==} @@ -16918,6 +16927,8 @@ snapshots: '@arr/every@1.0.1': {} + '@atomone/atomone-types-long@1.0.2': {} + '@atomone/govgen-types-long@0.3.9': {} '@aws-crypto/crc32@3.0.0': @@ -16978,8 +16989,8 @@ snapshots: '@aws-crypto/sha1-browser': 3.0.0 '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/client-sts@3.577.0) - '@aws-sdk/client-sts': 3.577.0 + '@aws-sdk/client-sso-oidc': 3.577.0 + '@aws-sdk/client-sts': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/core': 3.576.0 '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/middleware-bucket-endpoint': 3.577.0 @@ -17036,11 +17047,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.577.0(@aws-sdk/client-sts@3.577.0)': + '@aws-sdk/client-sso-oidc@3.577.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.577.0 + '@aws-sdk/client-sts': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/core': 3.576.0 '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -17079,7 +17090,6 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.7.0 transitivePeerDependencies: - - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso@3.577.0': @@ -17125,11 +17135,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.577.0': + '@aws-sdk/client-sts@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/client-sso-oidc': 3.577.0 '@aws-sdk/core': 3.576.0 '@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0) '@aws-sdk/middleware-host-header': 3.577.0 @@ -17168,6 +17178,7 @@ snapshots: '@smithy/util-utf8': 3.0.0 tslib: 2.7.0 transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/core@3.576.0': @@ -17201,7 +17212,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)(@aws-sdk/client-sts@3.577.0)': dependencies: - '@aws-sdk/client-sts': 3.577.0 + '@aws-sdk/client-sts': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/credential-provider-env': 3.577.0 '@aws-sdk/credential-provider-process': 3.577.0 '@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) @@ -17258,7 +17269,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.577.0(@aws-sdk/client-sts@3.577.0)': dependencies: - '@aws-sdk/client-sts': 3.577.0 + '@aws-sdk/client-sts': 3.577.0(@aws-sdk/client-sso-oidc@3.577.0) '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/types': 3.0.0 @@ -17396,7 +17407,7 @@ snapshots: '@aws-sdk/token-providers@3.577.0(@aws-sdk/client-sso-oidc@3.577.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/client-sts@3.577.0) + '@aws-sdk/client-sso-oidc': 3.577.0 '@aws-sdk/types': 3.577.0 '@smithy/property-provider': 3.0.0 '@smithy/shared-ini-file-loader': 3.0.0 @@ -30940,7 +30951,7 @@ snapshots: ethereumjs-util@4.5.1: dependencies: - bn.js: 4.12.0 + bn.js: 4.12.1 create-hash: 1.2.0 elliptic: 6.5.5 ethereum-cryptography: 0.1.3