Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/lib/addressIdentity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const BASE58_ALPHABET =
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
const BASE58_MAP = new Map(
[...BASE58_ALPHABET].map((char, index) => [char, index]),
);

function bytesToHex(bytes: Uint8Array): string {
return `0x${[...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
}

function decodeBase58(value: string): Uint8Array | null {
let bytes = [0];
for (const char of value) {
const value = BASE58_MAP.get(char);
if (value === undefined) return null;
let carry = value;
for (let i = 0; i < bytes.length; i += 1) {
const next = bytes[i] * 58 + carry;
bytes[i] = next & 0xff;
carry = next >> 8;
}
while (carry > 0) {
bytes.push(carry & 0xff);
carry >>= 8;
}
}

for (const char of value) {
if (char !== "1") break;
bytes.push(0);
}

return new Uint8Array(bytes.reverse());
}

function ss58PublicKeyHex(address: string): string | null {
const decoded = decodeBase58(address);
if (!decoded) return null;
const prefixLength = decoded[0] < 64 ? 1 : decoded[0] < 128 ? 2 : 0;
if (prefixLength === 0) return null;
const keyStart = prefixLength;
const keyEnd = keyStart + 32;
if (decoded.length < keyEnd + 1) return null;
return bytesToHex(decoded.slice(keyStart, keyEnd));
}

export function addressIdentityKey(address: string | null | undefined): string {
const normalized = (address ?? "").trim();
if (!normalized) return "";
return ss58PublicKeyHex(normalized) ?? normalized.toLowerCase();
}

export function addressesReferToSameIdentity(
left: string | null | undefined,
right: string | null | undefined,
): boolean {
const leftKey = addressIdentityKey(left);
const rightKey = addressIdentityKey(right);
return Boolean(leftKey && rightKey && leftKey === rightKey);
}
9 changes: 9 additions & 0 deletions src/lib/proposalVetoUi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function calculateCitizenVetoSupportPercent(input: {
vetoVotes: number;
eligibleCitizens: number;
}): number {
const eligibleCitizens = Math.max(0, input.eligibleCitizens);
if (eligibleCitizens === 0) return 0;
const vetoVotes = Math.max(0, input.vetoVotes);
return Math.round((vetoVotes / eligibleCitizens) * 100);
}
5 changes: 4 additions & 1 deletion src/pages/chambers/Chamber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { formatDate, formatDateTime } from "@/lib/dateTime";
import { formatLoadError } from "@/lib/errorFormatting";
import { NoDataYetBar } from "@/components/NoDataYetBar";
import { useAuth } from "@/app/auth/AuthContext";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";

const Chamber: React.FC = () => {
const { id } = useParams();
Expand Down Expand Up @@ -234,7 +235,9 @@ const Chamber: React.FC = () => {

const isMember = useMemo(() => {
if (!address || !data) return false;
return data.governors.some((gov) => gov.id === address);
return data.governors.some((gov) =>
addressesReferToSameIdentity(gov.id, address),
);
}, [address, data]);

const canWrite = useMemo(() => {
Expand Down
17 changes: 7 additions & 10 deletions src/pages/factions/Faction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,11 @@ import {
apiMe,
getApiErrorPayload,
} from "@/lib/apiClient";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";
import { formatDateTime } from "@/lib/dateTime";
import { formatLoadError } from "@/lib/errorFormatting";
import type { FactionDto } from "@/types/api";

function normalizeAddress(value: string): string {
return value.trim().toLowerCase();
}

const Faction: React.FC = () => {
const { id } = useParams();
const [searchParams] = useSearchParams();
Expand Down Expand Up @@ -96,10 +93,8 @@ const Faction: React.FC = () => {

const viewerMembership = useMemo(() => {
if (!viewerAddress) return null;
return memberships.find(
(membership) =>
normalizeAddress(membership.address) ===
normalizeAddress(viewerAddress),
return memberships.find((membership) =>
addressesReferToSameIdentity(membership.address, viewerAddress),
);
}, [memberships, viewerAddress]);

Expand Down Expand Up @@ -389,8 +384,10 @@ const Faction: React.FC = () => {
.map((membership) => {
const isSelf =
viewerAddress !== null &&
normalizeAddress(viewerAddress) ===
normalizeAddress(membership.address);
addressesReferToSameIdentity(
viewerAddress,
membership.address,
);
return (
<div
key={membership.address}
Expand Down
11 changes: 3 additions & 8 deletions src/pages/factions/FactionChannel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@ import {
apiMe,
getApiErrorPayload,
} from "@/lib/apiClient";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";
import { formatDateTime } from "@/lib/dateTime";
import { formatLoadError } from "@/lib/errorFormatting";
import type { FactionDto } from "@/types/api";

function normalizeAddress(value: string): string {
return value.trim().toLowerCase();
}

const FactionChannel: React.FC = () => {
const { id, channelId, threadId } = useParams();
const [faction, setFaction] = useState<FactionDto | null>(null);
Expand Down Expand Up @@ -64,10 +61,8 @@ const FactionChannel: React.FC = () => {

const viewerMembership = useMemo(() => {
if (!viewerAddress) return null;
return memberships.find(
(membership) =>
normalizeAddress(membership.address) ===
normalizeAddress(viewerAddress),
return memberships.find((membership) =>
addressesReferToSameIdentity(membership.address, viewerAddress),
);
}, [memberships, viewerAddress]);

Expand Down
23 changes: 10 additions & 13 deletions src/pages/factions/FactionInitiative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@ import {
apiMe,
getApiErrorPayload,
} from "@/lib/apiClient";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";
import { formatDateTime } from "@/lib/dateTime";
import { formatLoadError } from "@/lib/errorFormatting";
import type { FactionDto } from "@/types/api";

function normalizeAddress(value: string): string {
return value.trim().toLowerCase();
}

const FactionInitiative: React.FC = () => {
const { id, initiativeId } = useParams();
const [faction, setFaction] = useState<FactionDto | null>(null);
Expand Down Expand Up @@ -60,10 +57,8 @@ const FactionInitiative: React.FC = () => {

const viewerMembership = useMemo(() => {
if (!viewerAddress) return null;
return memberships.find(
(membership) =>
normalizeAddress(membership.address) ===
normalizeAddress(viewerAddress),
return memberships.find((membership) =>
addressesReferToSameIdentity(membership.address, viewerAddress),
);
}, [memberships, viewerAddress]);

Expand All @@ -73,10 +68,12 @@ const FactionInitiative: React.FC = () => {
viewerMembership?.role === "steward";

const initiatives = useMemo(() => {
const viewer = normalizeAddress(viewerAddress ?? "");
return initiativesRaw.filter((initiative) => {
if (initiative.status !== "draft") return true;
return normalizeAddress(initiative.ownerAddress) === viewer;
return addressesReferToSameIdentity(
initiative.ownerAddress,
viewerAddress,
);
});
}, [initiativesRaw, viewerAddress]);

Expand All @@ -87,9 +84,9 @@ const FactionInitiative: React.FC = () => {
const canEditActiveInitiative = useMemo(() => {
if (!activeInitiative) return false;
if (canModerate) return true;
return (
normalizeAddress(activeInitiative.ownerAddress) ===
normalizeAddress(viewerAddress ?? "")
return addressesReferToSameIdentity(
activeInitiative.ownerAddress,
viewerAddress,
);
}, [activeInitiative, canModerate, viewerAddress]);

Expand Down
8 changes: 2 additions & 6 deletions src/pages/factions/FactionInitiativeCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ import {
apiMe,
getApiErrorPayload,
} from "@/lib/apiClient";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";
import { formatLoadError } from "@/lib/errorFormatting";
import type { FactionDto } from "@/types/api";

function normalizeAddress(value: string): string {
return value.trim().toLowerCase();
}

const FactionInitiativeCreate: React.FC = () => {
const { id } = useParams();
const navigate = useNavigate();
Expand Down Expand Up @@ -62,8 +59,7 @@ const FactionInitiativeCreate: React.FC = () => {
return (faction.memberships ?? []).some(
(membership) =>
membership.isActive &&
normalizeAddress(membership.address) ===
normalizeAddress(viewerAddress),
addressesReferToSameIdentity(membership.address, viewerAddress),
);
}, [faction, viewerAddress]);

Expand Down
9 changes: 3 additions & 6 deletions src/pages/factions/FactionThreadCreate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ import {
apiMe,
getApiErrorPayload,
} from "@/lib/apiClient";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";
import { formatLoadError } from "@/lib/errorFormatting";
import type { FactionDto } from "@/types/api";

function normalizeAddress(value: string): string {
return value.trim().toLowerCase();
}

const FactionThreadCreate: React.FC = () => {
const { id, channelId } = useParams();
const navigate = useNavigate();
Expand Down Expand Up @@ -61,8 +58,8 @@ const FactionThreadCreate: React.FC = () => {
return (
(faction.memberships ?? []).find(
(membership) =>
normalizeAddress(membership.address) ===
normalizeAddress(viewerAddress) && membership.isActive,
addressesReferToSameIdentity(membership.address, viewerAddress) &&
membership.isActive,
) ?? null
);
}, [faction, viewerAddress]);
Expand Down
12 changes: 6 additions & 6 deletions src/pages/feed/Feed.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { cn } from "@/lib/utils";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";
import { useAuth } from "@/app/auth/AuthContext";
import { Button } from "@/components/primitives/button";
import { PageHint } from "@/components/PageHint";
Expand Down Expand Up @@ -110,18 +111,17 @@ const urgentEntityKey = (item: FeedItemDto) => {
return `id:${item.id}`;
};

const isUrgentItemInteractable = (
export const isUrgentItemInteractable = (
item: FeedItemDto,
isGovernorActive: boolean,
viewerAddress?: string,
) => {
if (item.actionable !== true) return false;
if (item.stage === "build") {
const viewer = viewerAddress?.trim().toLowerCase();
const proposer = (item.proposerId ?? item.proposer ?? "")
.trim()
.toLowerCase();
return Boolean(viewer && proposer && viewer === proposer);
return addressesReferToSameIdentity(
viewerAddress,
item.proposerId ?? item.proposer,
);
}
if ((item.stage === "pool" || item.stage === "vote") && !isGovernorActive) {
if (item.href?.includes("/referendum")) return true;
Expand Down
9 changes: 6 additions & 3 deletions src/pages/human-nodes/HumanNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
apiHuman,
apiMyGovernance,
} from "@/lib/apiClient";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";
import { formatLoadError } from "@/lib/errorFormatting";
import type {
GetMyGovernanceResponse,
Expand Down Expand Up @@ -251,7 +252,7 @@ const HumanNode: React.FC = () => {
);

const showShortBadge = !isAddressName && !isGenericName;
const isSelfProfile = Boolean(auth.address && profile.id === auth.address);
const isSelfProfile = addressesReferToSameIdentity(auth.address, profile.id);

const handleDelegateHere = async (chamberId: string) => {
if (!id) return;
Expand Down Expand Up @@ -437,8 +438,10 @@ const HumanNode: React.FC = () => {
{delegationCards.map((item) => {
const viewerItem =
viewerDelegationByChamber.get(item.chamberId) ?? null;
const viewerAlreadyDelegatesHere =
viewerItem?.delegateeAddress === profile.id;
const viewerAlreadyDelegatesHere = addressesReferToSameIdentity(
viewerItem?.delegateeAddress,
profile.id,
);
const canManage =
!isSelfProfile &&
manageableChambers.some(
Expand Down
8 changes: 5 additions & 3 deletions src/pages/proposals/ProposalChamber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
apiProposalChamberPage,
apiProposalTimeline,
} from "@/lib/apiClient";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";
import { formatLoadError } from "@/lib/errorFormatting";
import { formatDateTime } from "@/lib/dateTime";
import type {
Expand Down Expand Up @@ -161,9 +162,10 @@ const ProposalChamber: React.FC = () => {
? proposal.milestoneIndex
: null;
const referendumVote = proposal.voteKind === "referendum";
const viewerIsProposer =
auth.address?.trim().toLowerCase() ===
proposal.proposerId.trim().toLowerCase();
const viewerIsProposer = addressesReferToSameIdentity(
auth.address,
proposal.proposerId,
);
const scoreLabel =
proposal.scoreLabel === "MM" || milestoneVoteIndex !== null
? "MM"
Expand Down
8 changes: 5 additions & 3 deletions src/pages/proposals/ProposalChamberVeto.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
apiProposalChamberVetoPage,
apiProposalTimeline,
} from "@/lib/apiClient";
import { addressesReferToSameIdentity } from "@/lib/addressIdentity";
import { formatDateTime } from "@/lib/dateTime";
import { formatLoadError } from "@/lib/errorFormatting";
import type {
Expand Down Expand Up @@ -116,9 +117,10 @@ const ProposalChamberVeto: React.FC = () => {
);
}

const viewerIsProposer =
auth.address?.trim().toLowerCase() ===
proposal.proposerId.trim().toLowerCase();
const viewerIsProposer = addressesReferToSameIdentity(
auth.address,
proposal.proposerId,
);
const stageLinks = id
? {
vote: proposal.voteRoute,
Expand Down
Loading
Loading