diff --git a/projects/explorer/src/app/pages/proposals/proposal/proposal.component.html b/projects/explorer/src/app/pages/proposals/proposal/proposal.component.html index 1552b2696..43c362a1f 100644 --- a/projects/explorer/src/app/pages/proposals/proposal/proposal.component.html +++ b/projects/explorer/src/app/pages/proposals/proposal/proposal.component.html @@ -2,6 +2,9 @@ [proposal]="proposal$ | async" [proposalType]="proposalType$ | async" [deposits]="deposits$ | async" + [depositParams]="depositParams$ | async" [tally]="tally$ | async" + [tallyParams]="tallyParams$ | async" [votes]="votes$ | async" + [votingParams]="votingParams$ | async" > diff --git a/projects/explorer/src/app/pages/proposals/proposal/proposal.component.ts b/projects/explorer/src/app/pages/proposals/proposal/proposal.component.ts index 590a4cb5f..b472927bd 100644 --- a/projects/explorer/src/app/pages/proposals/proposal/proposal.component.ts +++ b/projects/explorer/src/app/pages/proposals/proposal/proposal.component.ts @@ -6,6 +6,9 @@ import { InlineResponse20054Deposits, InlineResponse20052FinalTallyResult, InlineResponse20057Votes, + InlineResponse20051DepositParams, + InlineResponse20051TallyParams, + InlineResponse20051VotingParams, } from '@cosmos-client/core/esm/openapi'; import { CosmosSDKService } from 'projects/explorer/src/app/models/cosmos-sdk.service'; import { combineLatest, Observable, of } from 'rxjs'; @@ -20,8 +23,11 @@ export class ProposalComponent implements OnInit { proposal$: Observable; proposalType$: Observable; deposits$: Observable; + depositParams$: Observable; tally$: Observable; + tallyParams$: Observable; votes$: Observable; + votingParams$: Observable; constructor(private route: ActivatedRoute, private cosmosSDK: CosmosSDKService) { const proposalID$ = this.route.params.pipe(map((params) => params.id)); @@ -53,6 +59,11 @@ export class ProposalComponent implements OnInit { }), ); + this.depositParams$ = this.cosmosSDK.sdk$.pipe( + mergeMap((sdk) => rest.gov.params(sdk.rest, 'deposit')), + map((result) => result.data.deposit_params), + ); + this.tally$ = combined$.pipe( mergeMap(([sdk, address]) => rest.gov.tallyresult(sdk.rest, address)), map((result) => result.data.tally!), @@ -62,6 +73,15 @@ export class ProposalComponent implements OnInit { }), ); + this.tallyParams$ = this.cosmosSDK.sdk$.pipe( + mergeMap((sdk) => rest.gov.params(sdk.rest, 'tallying')), + map((result) => result.data.tally_params), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ); + this.votes$ = combined$.pipe( mergeMap(([sdk, address]) => rest.gov.votes(sdk.rest, address)), map((result) => result.data.votes!), @@ -70,6 +90,15 @@ export class ProposalComponent implements OnInit { return of(undefined); }), ); + + this.votingParams$ = this.cosmosSDK.sdk$.pipe( + mergeMap((sdk) => rest.gov.params(sdk.rest, 'voting')), + map((result) => result.data.voting_params), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ); } ngOnInit(): void {} diff --git a/projects/explorer/src/app/views/proposals/proposal/proposal.component.html b/projects/explorer/src/app/views/proposals/proposal/proposal.component.html index 0ddff4845..5a3fe0ffa 100644 --- a/projects/explorer/src/app/views/proposals/proposal/proposal.component.html +++ b/projects/explorer/src/app/views/proposals/proposal/proposal.component.html @@ -56,7 +56,11 @@

Details

Description

- {{ content.description }} + + + {{ content.description }} + + @@ -93,57 +97,99 @@

Votes

+ + + + + Voting Period: + + {{ votingParams?.voting_period }} + + + + + Quorum: + + {{ tallyParams?.quorum?.substring(0, 5) }} + + + + + Threshold: + + {{ tallyParams?.threshold?.substring(0, 5) }} + + + + + Veto Threshold: + + {{ tallyParams?.veto_threshold?.substring(0, 5) }} + + + + +

deposits

-

Total

- No Deposit + + No Deposit + - {{ total.amount }} + Total Deposit: - {{ total.denom }} + {{ total.amount }} {{ total.denom }} + + + + + + Minimum Deposit: + + {{ min.amount }} {{ min.denom }} - + + + Max Deposit Period: + + {{ depositParams?.max_deposit_period }} + - -

Depositor: {{ deposit.depositor }}

- - - - No Deposit - + + + - {{ amount.amount }} + {{ deposit.depositor }}: - {{ amount.denom }} + {{ amount.amount }} {{ amount.denom }} - - - -
+ + + + - -

Vote

-

voter: {{ vote.voter }}

- - +

Vote

+ + + - Select: + {{ vote.voter }}: - {{ vote.option }} + {{ vote.option?.replace('VOTE_OPTION_', '') }} - - -
+ + + diff --git a/projects/explorer/src/app/views/proposals/proposal/proposal.component.ts b/projects/explorer/src/app/views/proposals/proposal/proposal.component.ts index caddb7574..ee53831b7 100644 --- a/projects/explorer/src/app/views/proposals/proposal/proposal.component.ts +++ b/projects/explorer/src/app/views/proposals/proposal/proposal.component.ts @@ -6,6 +6,9 @@ import { InlineResponse20054Deposits, InlineResponse20052FinalTallyResult, InlineResponse20057Votes, + InlineResponse20051DepositParams, + InlineResponse20051TallyParams, + InlineResponse20051VotingParams, } from '@cosmos-client/core/esm/openapi'; @Component({ @@ -21,9 +24,15 @@ export class ProposalComponent implements OnInit { @Input() deposits?: InlineResponse20054Deposits[] | null; @Input() + depositParams?: InlineResponse20051DepositParams | null; + @Input() tally?: InlineResponse20052FinalTallyResult | null; @Input() + tallyParams?: InlineResponse20051TallyParams | null; + @Input() votes?: InlineResponse20057Votes[] | null; + @Input() + votingParams?: InlineResponse20051VotingParams | null; constructor() {} diff --git a/projects/portal/src/app/app.module.ts b/projects/portal/src/app/app.module.ts index 052e9aa73..ef2abb248 100644 --- a/projects/portal/src/app/app.module.ts +++ b/projects/portal/src/app/app.module.ts @@ -5,6 +5,8 @@ import { AppDelegateFormDialogModule } from './pages/dialogs/delegate/delegate-f import { AppDelegateMenuDialogModule } from './pages/dialogs/delegate/delegate-menu-dialog/delegate-menu-dialog.module'; import { AppRedelegateFormDialogModule } from './pages/dialogs/delegate/redelegate-form-dialog/redelegate-form-dialog.module'; import { AppUndelegateFormDialogModule } from './pages/dialogs/delegate/undelegate-form-dialog/undelegate-form-dialog.module'; +import { AppDepositFormDialogModule } from './pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.module'; +import { AppVoteFormDialogModule } from './pages/dialogs/vote/vote-form-dialog/vote-form-dialog.module'; import { reducers, metaReducers } from './reducers'; import { TxFeeConfirmDialogModule } from './views/cosmos/tx-fee-confirm-dialog/tx-fee-confirm-dialog.module'; import { ConnectWalletCompletedDialogModule } from './views/dialogs/wallets/connect-wallet-completed-dialog/connect-wallet-completed-dialog.module'; @@ -64,6 +66,8 @@ import { LoadingDialogModule } from 'ng-loading-dialog'; AppDelegateMenuDialogModule, AppRedelegateFormDialogModule, AppUndelegateFormDialogModule, + AppVoteFormDialogModule, + AppDepositFormDialogModule, ], providers: [], bootstrap: [AppComponent], diff --git a/projects/portal/src/app/models/cosmos/gov.application.service.ts b/projects/portal/src/app/models/cosmos/gov.application.service.ts new file mode 100644 index 000000000..6a0534062 --- /dev/null +++ b/projects/portal/src/app/models/cosmos/gov.application.service.ts @@ -0,0 +1,317 @@ +import { convertHexStringToUint8Array } from '../../utils/converter'; +import { TxFeeConfirmDialogComponent } from '../../views/cosmos/tx-fee-confirm-dialog/tx-fee-confirm-dialog.component'; +import { WalletApplicationService } from '../wallets/wallet.application.service'; +import { StoredWallet } from '../wallets/wallet.model'; +import { GovService } from './gov.service'; +import { SimulatedTxResultResponse } from './tx-common.model'; +import { Injectable } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Router } from '@angular/router'; +import { proto } from '@cosmos-client/core'; +import { InlineResponse20075 } from '@cosmos-client/core/esm/openapi'; +import { LoadingDialogService } from 'ng-loading-dialog'; +import { VoteFormDialogComponent } from '../../pages/dialogs/vote/vote-form-dialog/vote-form-dialog.component'; +import { DepositFormDialogComponent } from '../../pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.component'; + +@Injectable({ + providedIn: 'root', +}) +export class GovApplicationService { + constructor( + private readonly router: Router, + private readonly snackBar: MatSnackBar, + private readonly dialog: MatDialog, + private readonly loadingDialog: LoadingDialogService, + private readonly gov: GovService, + private readonly walletApplicationService: WalletApplicationService, + ) {} + + async openVoteFormDialog(proposalID: number): Promise { + const txHash = await this.dialog + .open(VoteFormDialogComponent, { data: proposalID }) + .afterClosed() + .toPromise(); + await this.router.navigate(['txs', txHash]); + } + + async openDepositFormDialog(proposalID: number): Promise { + const txHash = await this.dialog + .open(DepositFormDialogComponent, { data: proposalID }) + .afterClosed() + .toPromise(); + await this.router.navigate(['txs', txHash]); + } + + // WIP + async submitProposal(minimumGasPrice: proto.cosmos.base.v1beta1.ICoin) { + const privateWallet: StoredWallet & { privateKey: string } = + await this.walletApplicationService.openUnunifiKeyFormDialog(); + if (!privateWallet || !privateWallet.privateKey) { + this.snackBar.open('Failed to get Wallet info from dialog! Tray again!', 'Close'); + return; + } + + const privateKey = convertHexStringToUint8Array(privateWallet.privateKey); + + if (!privateKey) { + this.snackBar.open('Invalid PrivateKey!', 'Close'); + return; + } + + // simulate + let simulatedResultData: SimulatedTxResultResponse; + let gas: proto.cosmos.base.v1beta1.ICoin; + let fee: proto.cosmos.base.v1beta1.ICoin; + + const dialogRefSimulating = this.loadingDialog.open('Simulating...'); + + try { + simulatedResultData = await this.gov.simulateToSubmitProposal( + privateWallet.key_type, + minimumGasPrice, + privateKey, + ); + gas = simulatedResultData.estimatedGasUsedWithMargin; + fee = simulatedResultData.estimatedFeeWithMargin; + } catch (error) { + console.error(error); + const errorMessage = `Tx simulation failed: ${(error as Error).toString()}`; + this.snackBar.open(`An error has occur: ${errorMessage}`, 'Close'); + return; + } finally { + dialogRefSimulating.close(); + } + + // ask the user to confirm the fee with a dialog + const txFeeConfirmedResult = await this.dialog + .open(TxFeeConfirmDialogComponent, { + data: { + fee, + isConfirmed: false, + }, + }) + .afterClosed() + .toPromise(); + + if (txFeeConfirmedResult === undefined || txFeeConfirmedResult.isConfirmed === false) { + this.snackBar.open('Tx was canceled', undefined, { duration: 6000 }); + return; + } + + const dialogRef = this.loadingDialog.open('Loading...'); + + let submitProposalResult: InlineResponse20075 | undefined; + let txHash: string | undefined; + + try { + submitProposalResult = await this.gov.SubmitProposal( + privateWallet.key_type, + gas, + fee, + privateKey, + ); + txHash = submitProposalResult.tx_response?.txhash; + if (txHash === undefined) { + throw Error('Invalid txHash!'); + } + } catch (error) { + console.error(error); + const msg = (error as Error).toString(); + this.snackBar.open(`An error has occur: ${msg}`, 'Close'); + return; + } finally { + dialogRef.close(); + } + + this.snackBar.open('Successfully submit proposal', undefined, { duration: 6000 }); + + await this.router.navigate(['txs', txHash]); + } + + async Vote( + proposalID: number, + voteOption: proto.cosmos.gov.v1beta1.VoteOption, + minimumGasPrice: proto.cosmos.base.v1beta1.ICoin, + ) { + const privateWallet: StoredWallet & { privateKey: string } = + await this.walletApplicationService.openUnunifiKeyFormDialog(); + if (!privateWallet || !privateWallet.privateKey) { + this.snackBar.open('Failed to get Wallet info from dialog! Tray again!', 'Close'); + return; + } + + const privateKey = convertHexStringToUint8Array(privateWallet.privateKey); + + if (!privateKey) { + this.snackBar.open('Invalid PrivateKey!', 'Close'); + return; + } + + // simulate + let simulatedResultData: SimulatedTxResultResponse; + let gas: proto.cosmos.base.v1beta1.ICoin; + let fee: proto.cosmos.base.v1beta1.ICoin; + + const dialogRefSimulating = this.loadingDialog.open('Simulating...'); + + try { + simulatedResultData = await this.gov.simulateToVote( + privateWallet.key_type, + proposalID, + voteOption, + minimumGasPrice, + privateKey, + ); + gas = simulatedResultData.estimatedGasUsedWithMargin; + fee = simulatedResultData.estimatedFeeWithMargin; + } catch (error) { + console.error(error); + const errorMessage = `Tx simulation failed: ${(error as Error).toString()}`; + this.snackBar.open(`An error has occur: ${errorMessage}`, 'Close'); + return; + } finally { + dialogRefSimulating.close(); + } + + // ask the user to confirm the fee with a dialog + const txFeeConfirmedResult = await this.dialog + .open(TxFeeConfirmDialogComponent, { + data: { + fee, + isConfirmed: false, + }, + }) + .afterClosed() + .toPromise(); + + if (txFeeConfirmedResult === undefined || txFeeConfirmedResult.isConfirmed === false) { + this.snackBar.open('Tx was canceled', undefined, { duration: 6000 }); + return; + } + + const dialogRef = this.loadingDialog.open('Sending'); + + let voteResult: InlineResponse20075 | undefined; + let txHash: string | undefined; + + try { + voteResult = await this.gov.Vote( + privateWallet.key_type, + proposalID, + voteOption, + gas, + fee, + privateKey, + ); + txHash = voteResult.tx_response?.txhash; + if (txHash === undefined) { + throw Error('Invalid txHash!'); + } + } catch (error) { + console.error(error); + const msg = (error as Error).toString(); + this.snackBar.open(`An error has occur: ${msg}`, 'Close'); + return; + } finally { + dialogRef.close(); + } + + this.snackBar.open('Successfully vote the proposal', undefined, { duration: 6000 }); + await this.router.navigate(['txs', txHash]); + } + + async Deposit( + proposalID: number, + amount: proto.cosmos.base.v1beta1.ICoin, + minimumGasPrice: proto.cosmos.base.v1beta1.ICoin, + ) { + const privateWallet: StoredWallet & { privateKey: string } = + await this.walletApplicationService.openUnunifiKeyFormDialog(); + if (!privateWallet || !privateWallet.privateKey) { + this.snackBar.open('Failed to get Wallet info from dialog! Tray again!', 'Close'); + return; + } + + const privateKey = convertHexStringToUint8Array(privateWallet.privateKey); + + if (!privateKey) { + this.snackBar.open('Invalid PrivateKey!', 'Close'); + return; + } + + // simulate + let simulatedResultData: SimulatedTxResultResponse; + let gas: proto.cosmos.base.v1beta1.ICoin; + let fee: proto.cosmos.base.v1beta1.ICoin; + + const dialogRefSimulating = this.loadingDialog.open('Simulating...'); + + try { + simulatedResultData = await this.gov.simulateToDeposit( + privateWallet.key_type, + proposalID, + amount, + minimumGasPrice, + privateKey, + ); + gas = simulatedResultData.estimatedGasUsedWithMargin; + fee = simulatedResultData.estimatedFeeWithMargin; + } catch (error) { + console.error(error); + const errorMessage = `Tx simulation failed: ${(error as Error).toString()}`; + this.snackBar.open(`An error has occur: ${errorMessage}`, 'Close'); + return; + } finally { + dialogRefSimulating.close(); + } + + // ask the user to confirm the fee with a dialog + const txFeeConfirmedResult = await this.dialog + .open(TxFeeConfirmDialogComponent, { + data: { + fee, + isConfirmed: false, + }, + }) + .afterClosed() + .toPromise(); + + if (txFeeConfirmedResult === undefined || txFeeConfirmedResult.isConfirmed === false) { + this.snackBar.open('Tx was canceled', undefined, { duration: 6000 }); + return; + } + + const dialogRef = this.loadingDialog.open('Sending'); + + let depositResult: InlineResponse20075 | undefined; + let txHash: string | undefined; + + try { + depositResult = await this.gov.Deposit( + privateWallet.key_type, + proposalID, + amount, + gas, + fee, + privateKey, + ); + txHash = depositResult.tx_response?.txhash; + if (txHash === undefined) { + throw Error('Invalid txHash!'); + } + } catch (error) { + console.error(error); + const msg = (error as Error).toString(); + this.snackBar.open(`An error has occur: ${msg}`, 'Close'); + return; + } finally { + dialogRef.close(); + } + + this.snackBar.open('Successfully deposit the proposal', undefined, { duration: 6000 }); + + return txHash; + // await this.router.navigate(['txs', txHash]); + } +} diff --git a/projects/portal/src/app/models/cosmos/gov.service.ts b/projects/portal/src/app/models/cosmos/gov.service.ts new file mode 100644 index 000000000..0b8ebf00a --- /dev/null +++ b/projects/portal/src/app/models/cosmos/gov.service.ts @@ -0,0 +1,332 @@ +import { CosmosSDKService } from '../cosmos-sdk.service'; +import { KeyType } from '../keys/key.model'; +import { KeyService } from '../keys/key.service'; +import { SimulatedTxResultResponse } from './tx-common.model'; +import { TxCommonService } from './tx-common.service'; +import { Injectable } from '@angular/core'; +import { cosmosclient, rest, proto } from '@cosmos-client/core'; +import { InlineResponse20075 } from '@cosmos-client/core/esm/openapi'; + +@Injectable({ + providedIn: 'root', +}) +export class GovService { + constructor( + private readonly cosmosSDK: CosmosSDKService, + private readonly key: KeyService, + private readonly txCommonService: TxCommonService, + ) {} + + // WIP Submit Proposal + async SubmitProposal( + keyType: KeyType, + gas: proto.cosmos.base.v1beta1.ICoin, + fee: proto.cosmos.base.v1beta1.ICoin, + privateKey: Uint8Array, + ): Promise { + const txBuilder = await this.buildSubmitProposal(keyType, gas, fee, privateKey); + return await this.txCommonService.announceTx(txBuilder); + } + + async simulateToSubmitProposal( + keyType: KeyType, + minimumGasPrice: proto.cosmos.base.v1beta1.ICoin, + privateKey: Uint8Array, + ): Promise { + const dummyFee: proto.cosmos.base.v1beta1.ICoin = { + denom: minimumGasPrice.denom, + amount: '1', + }; + const dummyGas: proto.cosmos.base.v1beta1.ICoin = { + denom: minimumGasPrice.denom, + amount: '1', + }; + const simulatedTxBuilder = await this.buildSubmitProposal( + keyType, + dummyGas, + dummyFee, + privateKey, + ); + return await this.txCommonService.simulateTx(simulatedTxBuilder, minimumGasPrice); + } + + async buildSubmitProposal( + keyType: KeyType, + gas: proto.cosmos.base.v1beta1.ICoin, + fee: proto.cosmos.base.v1beta1.ICoin, + privateKey: Uint8Array, + ): Promise { + const sdk = await this.cosmosSDK.sdk().then((sdk) => sdk.rest); + const privKey = this.key.getPrivKey(keyType, privateKey); + if (!privKey) { + throw Error('Invalid privateKey!'); + } + const pubKey = privKey.pubKey(); + const fromAddress = cosmosclient.AccAddress.fromPublicKey(pubKey); + + // get account info + const account = await rest.auth + .account(sdk, fromAddress) + .then( + (res) => + res.data.account && + (cosmosclient.codec.unpackCosmosAny( + res.data.account, + ) as proto.cosmos.auth.v1beta1.BaseAccount), + ) + .catch((_) => undefined); + + if (!(account instanceof proto.cosmos.auth.v1beta1.BaseAccount)) { + throw Error('Address not found'); + } + + // build tx + const msgProposal = new proto.cosmos.gov.v1beta1.MsgSubmitProposal({ + content: null, + initial_deposit: [], + proposer: fromAddress.toString(), + }); + + const txBody = new proto.cosmos.tx.v1beta1.TxBody({ + messages: [cosmosclient.codec.packAny(msgProposal)], + }); + const authInfo = new proto.cosmos.tx.v1beta1.AuthInfo({ + signer_infos: [ + { + public_key: cosmosclient.codec.packAny(pubKey), + mode_info: { + single: { + mode: proto.cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_DIRECT, + }, + }, + sequence: account.sequence, + }, + ], + fee: { + amount: [fee], + gas_limit: cosmosclient.Long.fromString(gas.amount ? gas.amount : '200000'), + }, + }); + + // sign + const txBuilder = new cosmosclient.TxBuilder(sdk, txBody, authInfo); + const signDocBytes = txBuilder.signDocBytes(account.account_number); + txBuilder.addSignature(privKey.sign(signDocBytes)); + + return txBuilder; + } + + // Vote + async Vote( + keyType: KeyType, + proposalID: number, + voteOption: proto.cosmos.gov.v1beta1.VoteOption, + gas: proto.cosmos.base.v1beta1.ICoin, + fee: proto.cosmos.base.v1beta1.ICoin, + privateKey: Uint8Array, + ): Promise { + const txBuilder = await this.buildVote(keyType, proposalID, voteOption, gas, fee, privateKey); + return await this.txCommonService.announceTx(txBuilder); + } + + async simulateToVote( + keyType: KeyType, + proposalID: number, + voteOption: proto.cosmos.gov.v1beta1.VoteOption, + minimumGasPrice: proto.cosmos.base.v1beta1.ICoin, + privateKey: Uint8Array, + ): Promise { + const dummyFee: proto.cosmos.base.v1beta1.ICoin = { + denom: minimumGasPrice.denom, + amount: '1', + }; + const dummyGas: proto.cosmos.base.v1beta1.ICoin = { + denom: minimumGasPrice.denom, + amount: '1', + }; + const simulatedTxBuilder = await this.buildVote( + keyType, + proposalID, + voteOption, + dummyGas, + dummyFee, + privateKey, + ); + return await this.txCommonService.simulateTx(simulatedTxBuilder, minimumGasPrice); + } + + async buildVote( + keyType: KeyType, + proposalID: number, + voteOption: proto.cosmos.gov.v1beta1.VoteOption, + gas: proto.cosmos.base.v1beta1.ICoin, + fee: proto.cosmos.base.v1beta1.ICoin, + privateKey: Uint8Array, + ): Promise { + const sdk = await this.cosmosSDK.sdk().then((sdk) => sdk.rest); + const privKey = this.key.getPrivKey(keyType, privateKey); + if (!privKey) { + throw Error('Invalid privateKey!'); + } + const pubKey = privKey.pubKey(); + const fromAddress = cosmosclient.AccAddress.fromPublicKey(pubKey); + + // get account info + const account = await rest.auth + .account(sdk, fromAddress) + .then( + (res) => + res.data.account && + (cosmosclient.codec.unpackCosmosAny( + res.data.account, + ) as proto.cosmos.auth.v1beta1.BaseAccount), + ) + .catch((_) => undefined); + + if (!(account instanceof proto.cosmos.auth.v1beta1.BaseAccount)) { + throw Error('Address not found'); + } + + // build tx + const msgVote = new proto.cosmos.gov.v1beta1.MsgVote({ + proposal_id: cosmosclient.Long.fromNumber(proposalID), + voter: fromAddress.toString(), + option: voteOption, + }); + + const txBody = new proto.cosmos.tx.v1beta1.TxBody({ + messages: [cosmosclient.codec.packAny(msgVote)], + }); + const authInfo = new proto.cosmos.tx.v1beta1.AuthInfo({ + signer_infos: [ + { + public_key: cosmosclient.codec.packAny(pubKey), + mode_info: { + single: { + mode: proto.cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_DIRECT, + }, + }, + sequence: account.sequence, + }, + ], + fee: { + amount: [fee], + gas_limit: cosmosclient.Long.fromString(gas.amount ? gas.amount : '200000'), + }, + }); + + // sign + const txBuilder = new cosmosclient.TxBuilder(sdk, txBody, authInfo); + const signDocBytes = txBuilder.signDocBytes(account.account_number); + txBuilder.addSignature(privKey.sign(signDocBytes)); + + return txBuilder; + } + + // Deposit + async Deposit( + keyType: KeyType, + proposalID: number, + amount: proto.cosmos.base.v1beta1.ICoin, + gas: proto.cosmos.base.v1beta1.ICoin, + fee: proto.cosmos.base.v1beta1.ICoin, + privateKey: Uint8Array, + ): Promise { + const txBuilder = await this.buildDeposit(keyType, proposalID, amount, gas, fee, privateKey); + return await this.txCommonService.announceTx(txBuilder); + } + + async simulateToDeposit( + keyType: KeyType, + proposalID: number, + amount: proto.cosmos.base.v1beta1.ICoin, + minimumGasPrice: proto.cosmos.base.v1beta1.ICoin, + privateKey: Uint8Array, + ): Promise { + const dummyFee: proto.cosmos.base.v1beta1.ICoin = { + denom: minimumGasPrice.denom, + amount: '1', + }; + const dummyGas: proto.cosmos.base.v1beta1.ICoin = { + denom: minimumGasPrice.denom, + amount: '1', + }; + const simulatedTxBuilder = await this.buildDeposit( + keyType, + proposalID, + amount, + dummyGas, + dummyFee, + privateKey, + ); + return await this.txCommonService.simulateTx(simulatedTxBuilder, minimumGasPrice); + } + + async buildDeposit( + keyType: KeyType, + proposalID: number, + amount: proto.cosmos.base.v1beta1.ICoin, + gas: proto.cosmos.base.v1beta1.ICoin, + fee: proto.cosmos.base.v1beta1.ICoin, + privateKey: Uint8Array, + ): Promise { + const sdk = await this.cosmosSDK.sdk().then((sdk) => sdk.rest); + const privKey = this.key.getPrivKey(keyType, privateKey); + if (!privKey) { + throw Error('Invalid privateKey!'); + } + const pubKey = privKey.pubKey(); + const fromAddress = cosmosclient.AccAddress.fromPublicKey(pubKey); + + // get account info + const account = await rest.auth + .account(sdk, fromAddress) + .then( + (res) => + res.data.account && + (cosmosclient.codec.unpackCosmosAny( + res.data.account, + ) as proto.cosmos.auth.v1beta1.BaseAccount), + ) + .catch((_) => undefined); + + if (!(account instanceof proto.cosmos.auth.v1beta1.BaseAccount)) { + throw Error('Address not found'); + } + + // build tx + const msgDeposit = new proto.cosmos.gov.v1beta1.MsgDeposit({ + proposal_id: cosmosclient.Long.fromNumber(proposalID), + depositor: fromAddress.toString(), + amount: [amount], + }); + + const txBody = new proto.cosmos.tx.v1beta1.TxBody({ + messages: [cosmosclient.codec.packAny(msgDeposit)], + }); + const authInfo = new proto.cosmos.tx.v1beta1.AuthInfo({ + signer_infos: [ + { + public_key: cosmosclient.codec.packAny(pubKey), + mode_info: { + single: { + mode: proto.cosmos.tx.signing.v1beta1.SignMode.SIGN_MODE_DIRECT, + }, + }, + sequence: account.sequence, + }, + ], + fee: { + amount: [fee], + gas_limit: cosmosclient.Long.fromString(gas.amount ? gas.amount : '200000'), + }, + }); + + // sign + const txBuilder = new cosmosclient.TxBuilder(sdk, txBody, authInfo); + const signDocBytes = txBuilder.signDocBytes(account.account_number); + txBuilder.addSignature(privKey.sign(signDocBytes)); + + return txBuilder; + } +} diff --git a/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.component.css b/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.component.html b/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.component.html new file mode 100644 index 000000000..77ed5b3c0 --- /dev/null +++ b/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.component.html @@ -0,0 +1,9 @@ + diff --git a/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.component.ts b/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.component.ts new file mode 100644 index 000000000..87e030b67 --- /dev/null +++ b/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.component.ts @@ -0,0 +1,78 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { cosmosclient, proto, rest } from '@cosmos-client/core'; +import { InlineResponse20052Proposals } from '@cosmos-client/core/esm/openapi'; +import { CosmosSDKService } from 'projects/portal/src/app/models'; +import { ConfigService } from 'projects/portal/src/app/models/config.service'; +import { GovApplicationService } from 'projects/portal/src/app/models/cosmos/gov.application.service'; +import { StoredWallet } from 'projects/portal/src/app/models/wallets/wallet.model'; +import { WalletService } from 'projects/portal/src/app/models/wallets/wallet.service'; +import { DepositOnSubmitEvent } from 'projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.component'; +import { combineLatest, Observable, of } from 'rxjs'; +import { catchError, filter, map, mergeMap } from 'rxjs/operators'; + +@Component({ + selector: 'app-deposit-form-dialog', + templateUrl: './deposit-form-dialog.component.html', + styleUrls: ['./deposit-form-dialog.component.css'], +}) +export class DepositFormDialogComponent implements OnInit { + proposal$: Observable; + currentStoredWallet$: Observable; + coins$: Observable; + uguuBalance$: Observable | undefined; + minimumGasPrices$: Observable; + proposalID: number | undefined; + + constructor( + @Inject(MAT_DIALOG_DATA) + public readonly data: number, + public matDialogRef: MatDialogRef, + private readonly cosmosSDK: CosmosSDKService, + private readonly walletService: WalletService, + private readonly configS: ConfigService, + private readonly govAppService: GovApplicationService, + ) { + this.proposalID = data; + this.proposal$ = this.cosmosSDK.sdk$.pipe( + mergeMap((sdk) => rest.gov.proposal(sdk.rest, String(this.proposalID))), + map((result) => result.data.proposal!), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ); + this.currentStoredWallet$ = this.walletService.currentStoredWallet$; + const address$ = this.currentStoredWallet$.pipe( + filter((wallet): wallet is StoredWallet => wallet !== undefined && wallet !== null), + map((wallet) => cosmosclient.AccAddress.fromString(wallet.address)), + ); + + this.coins$ = combineLatest([this.cosmosSDK.sdk$, address$]).pipe( + mergeMap(([sdk, address]) => rest.bank.allBalances(sdk.rest, address)), + map((result) => result.data.balances), + ); + this.uguuBalance$ = this.coins$.pipe( + map((coins) => { + const balance = coins?.find((coin) => coin.denom == 'uguu'); + return balance ? balance.amount! : '0'; + }), + ); + + this.minimumGasPrices$ = this.configS.config$.pipe(map((config) => config?.minimumGasPrices)); + } + + ngOnInit(): void {} + + async onSubmit($event: DepositOnSubmitEvent) { + if (!this.proposalID) { + return; + } + const txHash = await this.govAppService.Deposit( + this.proposalID, + $event.amount, + $event.minimumGasPrice, + ); + this.matDialogRef.close(txHash); + } +} diff --git a/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.module.ts b/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.module.ts new file mode 100644 index 000000000..9b879d81b --- /dev/null +++ b/projects/portal/src/app/pages/dialogs/vote/deposit-form-dialog/deposit-form-dialog.module.ts @@ -0,0 +1,10 @@ +import { DepositFormDialogComponent } from './deposit-form-dialog.component'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { DepositFormDialogModule } from 'projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.module'; + +@NgModule({ + declarations: [DepositFormDialogComponent], + imports: [CommonModule, DepositFormDialogModule], +}) +export class AppDepositFormDialogModule {} diff --git a/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.component.css b/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.component.html b/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.component.html new file mode 100644 index 000000000..d8d3469e8 --- /dev/null +++ b/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.component.html @@ -0,0 +1,12 @@ + diff --git a/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.component.ts b/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.component.ts new file mode 100644 index 000000000..637d82da4 --- /dev/null +++ b/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.component.ts @@ -0,0 +1,110 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { cosmosclient, proto, rest } from '@cosmos-client/core'; +import { InlineResponse20052Proposals } from '@cosmos-client/core/esm/openapi'; +import { CosmosSDKService } from 'projects/portal/src/app/models'; +import { ConfigService } from 'projects/portal/src/app/models/config.service'; +import { GovApplicationService } from 'projects/portal/src/app/models/cosmos/gov.application.service'; +import { StoredWallet } from 'projects/portal/src/app/models/wallets/wallet.model'; +import { WalletService } from 'projects/portal/src/app/models/wallets/wallet.service'; +import { combineLatest, Observable, of } from 'rxjs'; +import { catchError, filter, map, mergeMap } from 'rxjs/operators'; + +@Component({ + selector: 'app-vote-form-dialog', + templateUrl: './vote-form-dialog.component.html', + styleUrls: ['./vote-form-dialog.component.css'], +}) +export class VoteFormDialogComponent implements OnInit { + proposal$: Observable; + currentStoredWallet$: Observable; + coins$: Observable; + uguuBalance$: Observable | undefined; + minimumGasPrices$: Observable; + proposalID: number | undefined; + + constructor( + @Inject(MAT_DIALOG_DATA) + public readonly data: number, + public matDialogRef: MatDialogRef, + private readonly cosmosSDK: CosmosSDKService, + private readonly walletService: WalletService, + private readonly configS: ConfigService, + private readonly govAppService: GovApplicationService, + ) { + this.proposalID = data; + this.proposal$ = this.cosmosSDK.sdk$.pipe( + mergeMap((sdk) => rest.gov.proposal(sdk.rest, String(this.proposalID))), + map((result) => result.data.proposal!), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ); + this.currentStoredWallet$ = this.walletService.currentStoredWallet$; + const address$ = this.currentStoredWallet$.pipe( + filter((wallet): wallet is StoredWallet => wallet !== undefined && wallet !== null), + map((wallet) => cosmosclient.AccAddress.fromString(wallet.address)), + ); + + this.coins$ = combineLatest([this.cosmosSDK.sdk$, address$]).pipe( + mergeMap(([sdk, address]) => rest.bank.allBalances(sdk.rest, address)), + map((result) => result.data.balances), + ); + this.uguuBalance$ = this.coins$.pipe( + map((coins) => { + const balance = coins?.find((coin) => coin.denom == 'uguu'); + return balance ? balance.amount! : '0'; + }), + ); + + this.minimumGasPrices$ = this.configS.config$.pipe(map((config) => config?.minimumGasPrices)); + } + + ngOnInit(): void {} + + async onSubmitYes(gasPrice: proto.cosmos.base.v1beta1.ICoin) { + if (!this.proposalID) { + return; + } + const txHash = await this.govAppService.Vote( + this.proposalID, + proto.cosmos.gov.v1beta1.VoteOption.VOTE_OPTION_YES, + gasPrice, + ); + this.matDialogRef.close(txHash); + } + async onSubmitNoWithVeto(gasPrice: proto.cosmos.base.v1beta1.ICoin) { + if (!this.proposalID) { + return; + } + const txHash = await this.govAppService.Vote( + this.proposalID, + proto.cosmos.gov.v1beta1.VoteOption.VOTE_OPTION_NO_WITH_VETO, + gasPrice, + ); + this.matDialogRef.close(txHash); + } + async onSubmitNo(gasPrice: proto.cosmos.base.v1beta1.ICoin) { + if (!this.proposalID) { + return; + } + const txHash = await this.govAppService.Vote( + this.proposalID, + proto.cosmos.gov.v1beta1.VoteOption.VOTE_OPTION_NO, + gasPrice, + ); + this.matDialogRef.close(txHash); + } + async onSubmitAbstain(minimumGasPrice: proto.cosmos.base.v1beta1.ICoin) { + if (!this.proposalID) { + return; + } + const txHash = await this.govAppService.Vote( + this.proposalID, + proto.cosmos.gov.v1beta1.VoteOption.VOTE_OPTION_ABSTAIN, + minimumGasPrice, + ); + this.matDialogRef.close(txHash); + } +} diff --git a/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.module.ts b/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.module.ts new file mode 100644 index 000000000..0a6756338 --- /dev/null +++ b/projects/portal/src/app/pages/dialogs/vote/vote-form-dialog/vote-form-dialog.module.ts @@ -0,0 +1,10 @@ +import { VoteFormDialogComponent } from './vote-form-dialog.component'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { VoteFormDialogModule } from 'projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.module'; + +@NgModule({ + declarations: [VoteFormDialogComponent], + imports: [CommonModule, VoteFormDialogModule], +}) +export class AppVoteFormDialogModule {} diff --git a/projects/portal/src/app/pages/vote/proposals/proposal/proposal.component.html b/projects/portal/src/app/pages/vote/proposals/proposal/proposal.component.html index 6970de389..692b3b4eb 100644 --- a/projects/portal/src/app/pages/vote/proposals/proposal/proposal.component.html +++ b/projects/portal/src/app/pages/vote/proposals/proposal/proposal.component.html @@ -1 +1,12 @@ - + diff --git a/projects/portal/src/app/pages/vote/proposals/proposal/proposal.component.ts b/projects/portal/src/app/pages/vote/proposals/proposal/proposal.component.ts index 55364644f..e97b84c5c 100644 --- a/projects/portal/src/app/pages/vote/proposals/proposal/proposal.component.ts +++ b/projects/portal/src/app/pages/vote/proposals/proposal/proposal.component.ts @@ -6,8 +6,12 @@ import { InlineResponse20054Deposits, InlineResponse20052FinalTallyResult, InlineResponse20057Votes, + InlineResponse20051DepositParams, + InlineResponse20051TallyParams, + InlineResponse20051VotingParams, } from '@cosmos-client/core/esm/openapi'; import { CosmosSDKService } from 'projects/explorer/src/app/models/cosmos-sdk.service'; +import { GovApplicationService } from 'projects/portal/src/app/models/cosmos/gov.application.service'; import { combineLatest, Observable, of } from 'rxjs'; import { catchError, map, mergeMap } from 'rxjs/operators'; @@ -18,24 +22,39 @@ import { catchError, map, mergeMap } from 'rxjs/operators'; }) export class ProposalComponent implements OnInit { proposal$: Observable; + proposalType$: Observable; deposits$: Observable; + depositParams$: Observable; tally$: Observable; + tallyParams$: Observable; votes$: Observable; + votingParams$: Observable; - constructor(private route: ActivatedRoute, private cosmosSDK: CosmosSDKService) { + constructor( + private route: ActivatedRoute, + private cosmosSDK: CosmosSDKService, + private readonly govAppService: GovApplicationService, + ) { const proposalID$ = this.route.params.pipe(map((params) => params.id)); - console.log('proposalID'); - console.log(proposalID$); const combined$ = combineLatest([this.cosmosSDK.sdk$, proposalID$]); this.proposal$ = combined$.pipe( - mergeMap(([sdk, address]) => rest.gov.proposal(sdk.rest, address)), + mergeMap(([sdk, id]) => rest.gov.proposal(sdk.rest, id)), map((result) => result.data.proposal!), catchError((error) => { console.error(error); return of(undefined); }), ); + this.proposal$.subscribe((a) => console.log(a)); + + this.proposalType$ = this.proposal$.pipe( + map((proposal) => { + if (proposal && proposal.content) { + return (proposal.content as any)['@type']; + } + }), + ); this.deposits$ = combined$.pipe( mergeMap(([sdk, address]) => rest.gov.deposits(sdk.rest, address)), @@ -46,6 +65,11 @@ export class ProposalComponent implements OnInit { }), ); + this.depositParams$ = this.cosmosSDK.sdk$.pipe( + mergeMap((sdk) => rest.gov.params(sdk.rest, 'deposit')), + map((result) => result.data.deposit_params), + ); + this.tally$ = combined$.pipe( mergeMap(([sdk, address]) => rest.gov.tallyresult(sdk.rest, address)), map((result) => result.data.tally!), @@ -55,6 +79,15 @@ export class ProposalComponent implements OnInit { }), ); + this.tallyParams$ = this.cosmosSDK.sdk$.pipe( + mergeMap((sdk) => rest.gov.params(sdk.rest, 'tallying')), + map((result) => result.data.tally_params), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ); + this.votes$ = combined$.pipe( mergeMap(([sdk, address]) => rest.gov.votes(sdk.rest, address)), map((result) => result.data.votes!), @@ -63,7 +96,24 @@ export class ProposalComponent implements OnInit { return of(undefined); }), ); + + this.votingParams$ = this.cosmosSDK.sdk$.pipe( + mergeMap((sdk) => rest.gov.params(sdk.rest, 'voting')), + map((result) => result.data.voting_params), + catchError((error) => { + console.error(error); + return of(undefined); + }), + ); } ngOnInit(): void {} + + onVoteProposal(proposalID: number) { + this.govAppService.openVoteFormDialog(proposalID); + } + + onDepositProposal(proposalID: number) { + this.govAppService.openDepositFormDialog(proposalID); + } } diff --git a/projects/portal/src/app/pages/vote/proposals/proposals.component.html b/projects/portal/src/app/pages/vote/proposals/proposals.component.html index 5fc23b97f..f5b1bad39 100644 --- a/projects/portal/src/app/pages/vote/proposals/proposals.component.html +++ b/projects/portal/src/app/pages/vote/proposals/proposals.component.html @@ -1 +1,4 @@ - + diff --git a/projects/portal/src/app/pages/vote/proposals/proposals.component.ts b/projects/portal/src/app/pages/vote/proposals/proposals.component.ts index ccccb9537..ee3e0b7a0 100644 --- a/projects/portal/src/app/pages/vote/proposals/proposals.component.ts +++ b/projects/portal/src/app/pages/vote/proposals/proposals.component.ts @@ -1,3 +1,4 @@ +import { GovApplicationService } from '../../../models/cosmos/gov.application.service'; import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { rest } from '@cosmos-client/core'; @@ -17,6 +18,7 @@ export class ProposalsComponent implements OnInit { constructor( private readonly route: ActivatedRoute, private readonly cosmosSDK: CosmosSDKService, + private readonly govAppService: GovApplicationService, ) { this.proposals$ = this.cosmosSDK.sdk$.pipe( mergeMap((sdk) => rest.gov.proposals(sdk.rest)), @@ -25,4 +27,8 @@ export class ProposalsComponent implements OnInit { } ngOnInit(): void {} + + onVoteProposal(proposalID: number) { + this.govAppService.openVoteFormDialog(proposalID); + } } diff --git a/projects/portal/src/app/pages/vote/vote-routing.module.ts b/projects/portal/src/app/pages/vote/vote-routing.module.ts index d13cdd99e..f97613c70 100644 --- a/projects/portal/src/app/pages/vote/vote-routing.module.ts +++ b/projects/portal/src/app/pages/vote/vote-routing.module.ts @@ -11,7 +11,7 @@ const routes: Routes = [ canActivate: [WalletGuard], }, { - path: 'proposals/:address', + path: 'proposals/:id', component: ProposalComponent, canActivate: [WalletGuard], }, diff --git a/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.component.css b/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.component.html b/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.component.html new file mode 100644 index 000000000..6bfd4d830 --- /dev/null +++ b/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.component.html @@ -0,0 +1,111 @@ +
+ UnUniFi logo +
Deposit to this proposal
+
+ + + +
+ # {{ proposal?.proposal_id }} {{ content.title }} +
+
+
+
+ + + Depositor + + circle +
+ {{ currentStoredWallet?.address }} +
+
+
+ +
+
+ + Deposit Denom + + + + Deposit Amount + + + + Balance + + +
+ +

Gas Settings

+
+ + Minimum Gas Denom + + + {{ minimumGasPriceOption.denom }} + + + + + Minimum Gas Price + + +
+ + + + +
diff --git a/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.component.ts b/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.component.ts new file mode 100644 index 000000000..ddb634e9f --- /dev/null +++ b/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.component.ts @@ -0,0 +1,94 @@ +import { ProposalContent } from '../../../vote/proposals/proposals.component'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { cosmosclient, proto } from '@cosmos-client/core'; +import { InlineResponse20052Proposals } from '@cosmos-client/core/esm/openapi'; +import * as crypto from 'crypto'; +import { StoredWallet } from 'projects/portal/src/app/models/wallets/wallet.model'; + +export type DepositOnSubmitEvent = { + amount: proto.cosmos.base.v1beta1.ICoin; + minimumGasPrice: proto.cosmos.base.v1beta1.ICoin; +}; + +@Component({ + selector: 'view-deposit-form-dialog', + templateUrl: './deposit-form-dialog.component.html', + styleUrls: ['./deposit-form-dialog.component.css'], +}) +export class DepositFormDialogComponent implements OnInit { + @Input() + proposal?: InlineResponse20052Proposals | null; + @Input() + currentStoredWallet?: StoredWallet | null; + @Input() + coins?: proto.cosmos.base.v1beta1.ICoin[] | null; + @Input() + uguuBalance?: string | null; + @Input() + minimumGasPrices?: proto.cosmos.base.v1beta1.ICoin[] | null; + @Input() + proposalID?: number | null; + + @Output() + appSubmit: EventEmitter; + + selectedGasPrice?: proto.cosmos.base.v1beta1.ICoin; + availableDenoms?: string[]; + selectedAmount?: proto.cosmos.base.v1beta1.ICoin; + + constructor() { + this.appSubmit = new EventEmitter(); + // this.availableDenoms = this.coins?.map((coin) => coin.denom!); + this.availableDenoms = ['uguu']; + + this.selectedAmount = { denom: 'uguu', amount: '0' }; + } + + ngOnChanges(): void { + if (this.minimumGasPrices && this.minimumGasPrices.length > 0) { + this.selectedGasPrice = this.minimumGasPrices[0]; + } + } + + ngOnInit(): void {} + + getColorCode(address: string) { + const hash = crypto + .createHash('sha256') + .update(Buffer.from(address ?? '')) + .digest() + .toString('hex'); + return `#${hash.substr(0, 6)}`; + } + + unpackContent(value: any) { + try { + return cosmosclient.codec.unpackCosmosAny(value) as ProposalContent; + } catch { + return null; + } + } + + onSubmit() { + if (!this.selectedAmount) { + return; + } + if (this.selectedGasPrice === undefined) { + return; + } + this.selectedAmount.amount = this.selectedAmount.amount?.toString(); + this.appSubmit.emit({ amount: this.selectedAmount, minimumGasPrice: this.selectedGasPrice }); + } + + onMinimumGasDenomChanged(denom: string): void { + this.selectedGasPrice = this.minimumGasPrices?.find( + (minimumGasPrice) => minimumGasPrice.denom === denom, + ); + } + + onMinimumGasAmountSliderChanged(amount: string): void { + if (this.selectedGasPrice) { + this.selectedGasPrice.amount = amount; + } + } +} diff --git a/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.module.ts b/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.module.ts new file mode 100644 index 000000000..7db9fde16 --- /dev/null +++ b/projects/portal/src/app/views/dialogs/vote/deposit/deposit-form-dialog.module.ts @@ -0,0 +1,12 @@ +import { MaterialModule } from '../../../material.module'; +import { DepositFormDialogComponent } from './deposit-form-dialog.component'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +@NgModule({ + declarations: [DepositFormDialogComponent], + imports: [CommonModule, FormsModule, MaterialModule], + exports: [DepositFormDialogComponent], +}) +export class DepositFormDialogModule {} diff --git a/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.component.css b/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.component.html b/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.component.html new file mode 100644 index 000000000..ede9e213c --- /dev/null +++ b/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.component.html @@ -0,0 +1,86 @@ +
+ UnUniFi logo +
Vote to this proposal
+
+ + + +
+ # {{ proposal?.proposal_id }} {{ content.title }} +
+
+
+
+ + + Voter + + circle +
+ {{ currentStoredWallet?.address }} +
+
+
+ +

Gas Settings

+
+ + Minimum Gas Denom + + + {{ minimumGasPriceOption.denom }} + + + + + Minimum Gas Price + + +
+ + + + + + + + + + diff --git a/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.component.ts b/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.component.ts new file mode 100644 index 000000000..a9d289dec --- /dev/null +++ b/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.component.ts @@ -0,0 +1,112 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { cosmosclient, proto } from '@cosmos-client/core'; +import { InlineResponse20052Proposals } from '@cosmos-client/core/esm/openapi'; +import * as crypto from 'crypto'; +import { StoredWallet } from 'projects/portal/src/app/models/wallets/wallet.model'; +import { ProposalContent } from '../../../vote/proposals/proposals.component'; + +@Component({ + selector: 'view-vote-form-dialog', + templateUrl: './vote-form-dialog.component.html', + styleUrls: ['./vote-form-dialog.component.css'], +}) +export class VoteFormDialogComponent implements OnInit { + @Input() + proposal?: InlineResponse20052Proposals | null; + @Input() + currentStoredWallet?: StoredWallet | null; + @Input() + coins?: proto.cosmos.base.v1beta1.ICoin[] | null; + @Input() + uguuBalance?: string | null; + @Input() + minimumGasPrices?: proto.cosmos.base.v1beta1.ICoin[] | null; + @Input() + proposalID?: number | null; + + @Output() + appSubmitYes: EventEmitter; + @Output() + appSubmitNoWithVeto: EventEmitter; + @Output() + appSubmitNo: EventEmitter; + @Output() + appSubmitAbstain: EventEmitter; + + selectedGasPrice?: proto.cosmos.base.v1beta1.ICoin; + availableDenoms?: string[]; + selectedAmount?: proto.cosmos.base.v1beta1.ICoin; + + constructor() { + this.appSubmitYes = new EventEmitter(); + this.appSubmitNoWithVeto = new EventEmitter(); + this.appSubmitNo = new EventEmitter(); + this.appSubmitAbstain = new EventEmitter(); + // this.availableDenoms = this.coins?.map((coin) => coin.denom!); + this.availableDenoms = ['uguu']; + + this.selectedAmount = { denom: 'uguu', amount: '0' }; + } + + ngOnChanges(): void { + if (this.minimumGasPrices && this.minimumGasPrices.length > 0) { + this.selectedGasPrice = this.minimumGasPrices[0]; + } + } + + ngOnInit(): void {} + + getColorCode(address: string) { + const hash = crypto + .createHash('sha256') + .update(Buffer.from(address ?? '')) + .digest() + .toString('hex'); + return `#${hash.substr(0, 6)}`; + } + + unpackContent(value: any) { + try { + return cosmosclient.codec.unpackCosmosAny(value) as ProposalContent; + } catch { + return null; + } + } + + onSubmitYes() { + if (this.selectedGasPrice === undefined) { + return; + } + this.appSubmitYes.emit(this.selectedGasPrice); + } + onSubmitNoWithVeto() { + if (this.selectedGasPrice === undefined) { + return; + } + this.appSubmitYes.emit(this.selectedGasPrice); + } + onSubmitNo() { + if (this.selectedGasPrice === undefined) { + return; + } + this.appSubmitYes.emit(this.selectedGasPrice); + } + onSubmitAbstain() { + if (this.selectedGasPrice === undefined) { + return; + } + this.appSubmitYes.emit(this.selectedGasPrice); + } + + onMinimumGasDenomChanged(denom: string): void { + this.selectedGasPrice = this.minimumGasPrices?.find( + (minimumGasPrice) => minimumGasPrice.denom === denom, + ); + } + + onMinimumGasAmountSliderChanged(amount: string): void { + if (this.selectedGasPrice) { + this.selectedGasPrice.amount = amount; + } + } +} diff --git a/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.module.ts b/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.module.ts new file mode 100644 index 000000000..2ee74064d --- /dev/null +++ b/projects/portal/src/app/views/dialogs/vote/vote/vote-form-dialog.module.ts @@ -0,0 +1,12 @@ +import { MaterialModule } from '../../../material.module'; +import { VoteFormDialogComponent } from './vote-form-dialog.component'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +@NgModule({ + declarations: [VoteFormDialogComponent], + imports: [CommonModule, FormsModule, MaterialModule], + exports: [VoteFormDialogComponent], +}) +export class VoteFormDialogModule {} diff --git a/projects/portal/src/app/views/vote/proposals/proposal/proposal.component.html b/projects/portal/src/app/views/vote/proposals/proposal/proposal.component.html index ef02161af..e95ad62aa 100644 --- a/projects/portal/src/app/views/vote/proposals/proposal/proposal.component.html +++ b/projects/portal/src/app/views/vote/proposals/proposal/proposal.component.html @@ -3,23 +3,70 @@ -

proposal: {{ proposal?.proposal_id }}

+ +
+

# {{ proposal?.proposal_id }} {{ content.title }}

+ + + {{ + proposal?.status?.replace('PROPOSAL_STATUS_', '') + }} + +
- - - - - Status: - - {{ proposal?.status }} - - - - +

Details

+ + + + + Type + + {{ + proposalType?.substring(proposalType?.lastIndexOf('.')! + 1, proposalType?.length) + }} + + + + Submitted: + + {{ proposal?.submit_time }} + + + + Start Voting: + + {{ proposal?.voting_start_time }} + + + + End Voting: + + {{ proposal?.voting_end_time }} + + + + Deposit End Time: + + {{ proposal?.deposit_end_time }} + + + + -

Tally

+

Description

+ + + + + {{ content.description }} + + + + +
- +

Votes

+ @@ -29,10 +76,11 @@

Tally

- Abstain: + No with Veto: - {{ tally?.abstain }} + {{ tally?.no_with_veto }} + No: @@ -41,91 +89,127 @@

Tally

- No with Veto: + Abstain: - {{ tally?.no_with_veto }} + {{ tally?.abstain }}
-

Deposit

- + - Submit Time: + Voting Period: - {{ proposal?.submit_time }} + {{ votingParams?.voting_period }} + - Deposit End Time: + Quorum: - {{ proposal?.deposit_end_time }} + {{ tallyParams?.quorum?.substring(0, 5) }} - - - + -

Total Deposit

- - - - - {{ total.amount }} + + Threshold: - {{ total.denom }} + {{ tallyParams?.threshold?.substring(0, 5) }} - - - - -

Depositor: {{ deposit.depositor }}

- - - - {{ amount.amount }} + + Veto Threshold: - {{ amount.denom }} + {{ tallyParams?.veto_threshold?.substring(0, 5) }} - - -
+ +
-

Vote

- + + +

deposits

+ - - Vote Start Time: - - {{ proposal?.voting_start_time }} - + + + No Deposit + + + + + Total Deposit: + + {{ total.amount }} {{ total.denom }} + + + + + + Minimum Deposit: + + {{ min.amount }} {{ min.denom }} + + - Vote End Time: + Max Deposit Period: - {{ proposal?.voting_end_time }} + {{ depositParams?.max_deposit_period }} - -

voter: {{ vote.voter }}

- - + + + + + + {{ deposit.depositor }}: + + {{ amount.amount }} {{ amount.denom }} + + + + + + + + + +

Vote

+ + + - Yes: + {{ vote.voter }}: - {{ vote.option }} + {{ vote.option?.replace('VOTE_OPTION_', '') }} - - -
+ + +
diff --git a/projects/portal/src/app/views/vote/proposals/proposal/proposal.component.ts b/projects/portal/src/app/views/vote/proposals/proposal/proposal.component.ts index 464851b9a..b3881db49 100644 --- a/projects/portal/src/app/views/vote/proposals/proposal/proposal.component.ts +++ b/projects/portal/src/app/views/vote/proposals/proposal/proposal.component.ts @@ -1,9 +1,14 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { ProposalContent } from '../proposals.component'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { cosmosclient } from '@cosmos-client/core'; import { InlineResponse20052Proposals, InlineResponse20054Deposits, InlineResponse20052FinalTallyResult, InlineResponse20057Votes, + InlineResponse20051DepositParams, + InlineResponse20051TallyParams, + InlineResponse20051VotingParams, } from '@cosmos-client/core/esm/openapi'; @Component({ @@ -15,13 +20,45 @@ export class ProposalComponent implements OnInit { @Input() proposal?: InlineResponse20052Proposals | null; @Input() + proposalType?: string | null; + @Input() deposits?: InlineResponse20054Deposits[] | null; @Input() + depositParams?: InlineResponse20051DepositParams | null; + @Input() tally?: InlineResponse20052FinalTallyResult | null; @Input() + tallyParams?: InlineResponse20051TallyParams | null; + @Input() votes?: InlineResponse20057Votes[] | null; + @Input() + votingParams?: InlineResponse20051VotingParams | null; - constructor() {} + @Output() + appClickVote: EventEmitter; + @Output() + appClickDeposit: EventEmitter; + + constructor() { + this.appClickVote = new EventEmitter(); + this.appClickDeposit = new EventEmitter(); + } ngOnInit(): void {} + + unpackContent(value: any) { + try { + return cosmosclient.codec.unpackCosmosAny(value) as ProposalContent; + } catch { + return null; + } + } + + onClickVote(proposalID: string) { + this.appClickVote.emit(Number(proposalID)); + } + + onClickDeposit(proposalID: string) { + this.appClickDeposit.emit(Number(proposalID)); + } } diff --git a/projects/portal/src/app/views/vote/proposals/proposal/proposal.module.ts b/projects/portal/src/app/views/vote/proposals/proposal/proposal.module.ts index 424ed1f91..ca65ed139 100644 --- a/projects/portal/src/app/views/vote/proposals/proposal/proposal.module.ts +++ b/projects/portal/src/app/views/vote/proposals/proposal/proposal.module.ts @@ -2,11 +2,12 @@ import { MaterialModule } from '../../../material.module'; import { ProposalComponent } from './proposal.component'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatChipsModule } from '@angular/material/chips'; import { RouterModule } from '@angular/router'; @NgModule({ declarations: [ProposalComponent], - imports: [CommonModule, RouterModule, MaterialModule], + imports: [CommonModule, RouterModule, MaterialModule, MatChipsModule], exports: [ProposalComponent], }) export class ProposalModule {} diff --git a/projects/portal/src/app/views/vote/proposals/proposals.component.html b/projects/portal/src/app/views/vote/proposals/proposals.component.html index 8aba249fb..661006d6e 100644 --- a/projects/portal/src/app/views/vote/proposals/proposals.component.html +++ b/projects/portal/src/app/views/vote/proposals/proposals.component.html @@ -14,12 +14,29 @@

Proposals

- - {{ proposal.proposal_id }} - - {{ proposal.status }} - - + + + # {{ proposal.proposal_id }} + + {{ content.title }} + + {{ proposal.status?.replace('PROPOSAL_STATUS_', '') }} + + + thumb_up + {{ proposal.final_tally_result?.yes }} + + thumb_down + {{ proposal.final_tally_result?.no_with_veto }} + + thumb_down + {{ proposal.final_tally_result?.no }} + + subtitles_off + {{ proposal.final_tally_result?.abstain }} + + + diff --git a/projects/portal/src/app/views/vote/proposals/proposals.component.ts b/projects/portal/src/app/views/vote/proposals/proposals.component.ts index 1786f7732..b3fdc17c8 100644 --- a/projects/portal/src/app/views/vote/proposals/proposals.component.ts +++ b/projects/portal/src/app/views/vote/proposals/proposals.component.ts @@ -1,6 +1,13 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { cosmosclient } from '@cosmos-client/core'; import { InlineResponse20052 } from '@cosmos-client/core/esm/openapi'; +export interface ProposalContent { + type: string; + title: string; + description: string; +} + @Component({ selector: 'view-proposals', templateUrl: './proposals.component.html', @@ -10,7 +17,24 @@ export class ProposalsComponent implements OnInit { @Input() proposals?: InlineResponse20052 | null; - constructor() {} + @Output() + appClickVote: EventEmitter; + + constructor() { + this.appClickVote = new EventEmitter(); + } ngOnInit(): void {} + + unpackContent(value: any) { + try { + return cosmosclient.codec.unpackCosmosAny(value) as ProposalContent; + } catch { + return null; + } + } + + onClickVote(proposalID: string) { + this.appClickVote.emit(Number(proposalID)); + } }