From 671083d93c140711d60806b153dabebc0c6a8527 Mon Sep 17 00:00:00 2001 From: gRoussac Date: Fri, 28 Aug 2026 22:38:28 +0200 Subject: [PATCH] fix(aquachain): humanize LocalDAO contract errors in UI Map Already voted and related chain errors to short toasts; block duplicate votes after wallet connect when vote is already on-chain. Co-authored-by: Cursor --- .../src/app/components/local-dao/local-dao.ts | 53 +++++++++++++------ .../services/contract/contract-errors.spec.ts | 16 ++++++ .../app/services/contract/contract-errors.ts | 36 +++++++++++++ 3 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 apps/aquachain/src/app/services/contract/contract-errors.spec.ts create mode 100644 apps/aquachain/src/app/services/contract/contract-errors.ts diff --git a/apps/aquachain/src/app/components/local-dao/local-dao.ts b/apps/aquachain/src/app/components/local-dao/local-dao.ts index 3648fde..cd4a49d 100644 --- a/apps/aquachain/src/app/components/local-dao/local-dao.ts +++ b/apps/aquachain/src/app/components/local-dao/local-dao.ts @@ -40,6 +40,7 @@ import { voteCount, VoteChoice, } from '@services/local-dao/local-dao'; +import { humanizeContractError } from '@services/contract/contract-errors'; import { ModuleShell } from '../module-shell/module-shell'; import { WalletBanner } from '../shared/wallet-banner/wallet-banner'; @@ -248,6 +249,20 @@ export class LocalDao implements OnInit { if (!this.selected) { return; } + try { + await this.ensureWallet(); + await this.refreshWalletVote(); + } catch (err: unknown) { + this.toastr.showError(humanizeContractError(err), 'Wallet Connection Failed'); + return; + } + if (this.walletVote) { + this.toastr.showError( + `You already voted ${this.walletVote} on this proposal.`, + 'Already voted', + ); + return; + } await this.runAction( () => this.dao.vote( @@ -341,18 +356,7 @@ export class LocalDao implements OnInit { this.selected.id, this.walletAddress, ); - const vote = record.vote; - if (typeof vote === 'object' && vote !== null && !Array.isArray(vote)) { - if ('yes' in vote) { - this.walletVote = 'yes'; - } else if ('no' in vote) { - this.walletVote = 'no'; - } else { - this.walletVote = 'abstain'; - } - } else { - this.walletVote = null; - } + this.walletVote = normalizeVoteChoice(record.vote); } catch { this.walletVote = null; } @@ -388,10 +392,7 @@ export class LocalDao implements OnInit { } } } catch (err: unknown) { - this.toastr.showError( - err instanceof Error ? err.message : String(err), - errorTitle, - ); + this.toastr.showError(humanizeContractError(err), errorTitle); } finally { this.busyAction = false; } @@ -406,3 +407,23 @@ export class LocalDao implements OnInit { return address; } } + +function normalizeVoteChoice( + vote: VoteChoice | { yes?: null } | { no?: null } | { abstain?: null }, +): VoteChoice | null { + if (typeof vote === 'string') { + return vote; + } + if (vote && typeof vote === 'object') { + if ('yes' in vote) { + return 'yes'; + } + if ('no' in vote) { + return 'no'; + } + if ('abstain' in vote) { + return 'abstain'; + } + } + return null; +} diff --git a/apps/aquachain/src/app/services/contract/contract-errors.spec.ts b/apps/aquachain/src/app/services/contract/contract-errors.spec.ts new file mode 100644 index 0000000..5683b67 --- /dev/null +++ b/apps/aquachain/src/app/services/contract/contract-errors.spec.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; +import { humanizeContractError } from './contract-errors'; + +describe('humanizeContractError', () => { + it('maps already voted', () => { + expect(humanizeContractError(new Error('Generic error: Already voted'))).toBe( + 'You already voted on this proposal.', + ); + }); + + it('maps voting not ended', () => { + expect(humanizeContractError('Voting period has not ended')).toBe( + 'Voting is still open. Finalize after the end time.', + ); + }); +}); diff --git a/apps/aquachain/src/app/services/contract/contract-errors.ts b/apps/aquachain/src/app/services/contract/contract-errors.ts new file mode 100644 index 0000000..68b4593 --- /dev/null +++ b/apps/aquachain/src/app/services/contract/contract-errors.ts @@ -0,0 +1,36 @@ +/** Map CosmWasm / RPC errors to short user-facing copy. */ +export function humanizeContractError(raw: unknown): string { + const text = + raw instanceof Error ? raw.message : typeof raw === 'string' ? raw : String(raw); + + const rules: readonly [RegExp, string][] = [ + [/Already voted/i, 'You already voted on this proposal.'], + [/Voting period has not ended/i, 'Voting is still open. Finalize after the end time.'], + [/Voting period has ended/i, 'Voting has ended. You can no longer cast a vote.'], + [/Already finalized|Proposal already finalized/i, 'This proposal is already finalized.'], + [/Proposal is not open/i, 'This proposal is no longer open for votes.'], + [/Quorum not reached/i, 'Quorum was not reached. This proposal failed.'], + [/Proposal did not pass/i, 'The proposal did not pass.'], + [/Missing funds|Attached funds are required/i, 'Attach the reward OSMO amount in Keplr.'], + [/Invalid funds|do not match the proposal reward/i, 'Attached OSMO must match the proposal reward.'], + [/Invalid type map/i, 'App version mismatch. Hard-refresh the page and try again.'], + [/Keplr extension not found/i, 'Install and unlock the Keplr extension.'], + [/insufficient funds|spendable balance/i, 'Not enough OSMO for gas or the attached reward.'], + ]; + + for (const [pattern, message] of rules) { + if (pattern.test(text)) { + return message; + } + } + + if (text.length > 180) { + const generic = text.match(/Generic error: ([^:]+)/i)?.[1]?.trim(); + if (generic) { + return generic; + } + return 'Transaction failed. Check Keplr and try again.'; + } + + return text; +}