diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/InstructionsSummary.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/InstructionsSummary.tsx
index 828ab3a5e6..407f4fbcd4 100644
--- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/InstructionsSummary.tsx
+++ b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/InstructionsSummary.tsx
@@ -1,6 +1,13 @@
+import { Listbox, Transition } from '@headlessui/react'
import { PythCluster } from '@pythnetwork/client'
import { MultisigInstruction } from '@pythnetwork/xc-admin-common'
import { getInstructionsSummary } from './utils'
+import { getMappingCluster } from '../../InstructionViews/utils'
+import CopyText from '../../common/CopyText'
+import Arrow from '@images/icons/down.inline.svg'
+import { Fragment, useState, useMemo, useContext } from 'react'
+import { usePythContext } from '../../../contexts/PythContext'
+import { ClusterContext } from '../../../contexts/ClusterContext'
export const InstructionsSummary = ({
instructions,
@@ -8,18 +15,197 @@ export const InstructionsSummary = ({
}: {
instructions: MultisigInstruction[]
cluster: PythCluster
+}) => (
+
+ {getInstructionsSummary({ instructions, cluster }).map((instruction) => (
+
+ ))}
+
+)
+
+const SummaryItem = ({
+ instruction,
+}: {
+ instruction: ReturnType[number]
}) => {
- const instructionsCount = getInstructionsSummary({ instructions, cluster })
+ switch (instruction.name) {
+ case 'addPublisher':
+ case 'delPublisher': {
+ return (
+
+
+ {instruction.name}: {instruction.count}
+
+
+
+ )
+ }
+ default: {
+ return (
+
+ {instruction.name}: {instruction.count}
+
+ )
+ }
+ }
+}
+
+type AddRemovePublisherDetailsProps = {
+ isAdd: boolean
+ summaries: {
+ readonly priceAccount: string
+ readonly pub: string
+ }[]
+}
+
+const AddRemovePublisherDetails = ({
+ isAdd,
+ summaries,
+}: AddRemovePublisherDetailsProps) => {
+ const { cluster } = useContext(ClusterContext)
+ const { priceAccountKeyToSymbolMapping, publisherKeyToNameMapping } =
+ usePythContext()
+ const publisherKeyToName =
+ publisherKeyToNameMapping[getMappingCluster(cluster)]
+ const [groupBy, setGroupBy] = useState<'publisher' | 'price account'>(
+ 'publisher'
+ )
+ const grouped = useMemo(
+ () =>
+ Object.groupBy(summaries, (summary) =>
+ groupBy === 'publisher' ? summary.pub : summary.priceAccount
+ ),
+ [groupBy, summaries]
+ )
return (
-
- {Object.entries(instructionsCount).map(([name, count]) => {
- return (
-
- {name}: {count}
+
+
+
+
{groupBy === 'publisher' ? 'Publisher' : 'Price Account'}
+
+ {groupBy === 'publisher'
+ ? isAdd
+ ? 'Added To'
+ : 'Removed From'
+ : `${isAdd ? 'Added' : 'Removed'} Publishers`}
+
+
+ {Object.entries(grouped).map(([groupKey, summaries = []]) => (
+ <>
+
+
+
+ {groupKey}
+
+
+
+ {summaries.map((summary, index) => (
+ -
+
+ {groupBy === 'publisher'
+ ? summary.priceAccount
+ : summary.pub}
+
+
+ ))}
+
- )
- })}
+ >
+ ))}
+
+ )
+}
+
+const KeyAndName = ({
+ mapping,
+ children,
+}: {
+ mapping: { [key: string]: string }
+ children: string
+}) => {
+ const name = useMemo(() => mapping[children], [mapping, children])
+
+ return (
+
)
}
+
+type SelectProps
= {
+ items: T[]
+ value: T
+ onChange: (newValue: T) => void
+}
+
+const Select = ({
+ items,
+ value,
+ onChange,
+}: SelectProps) => (
+
+ {({ open }) => (
+ <>
+
+ {value}
+
+
+
+
+ {items.map((item) => (
+
+ {item}
+
+ ))}
+
+
+ >
+ )}
+
+)
diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/ProposalRow.tsx b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/ProposalRow.tsx
index 7f1a4406d9..1b0635ffa1 100644
--- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/ProposalRow.tsx
+++ b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/ProposalRow.tsx
@@ -23,7 +23,8 @@ export const ProposalRow = ({
multisig: MultisigAccount | undefined
}) => {
const [time, setTime] = useState()
- const [instructions, setInstructions] = useState<[string, number][]>()
+ const [instructions, setInstructions] =
+ useState<(readonly [string, number])[]>()
const status = getProposalStatus(proposal, multisig)
const { cluster } = useContext(ClusterContext)
const { isLoading: isMultisigLoading, connection } = useMultisigContext()
@@ -92,12 +93,13 @@ export const ProposalRow = ({
// show only the first two instructions
// and group the rest under 'other'
- const shortSummary = Object.entries(summary).slice(0, 2)
- const otherValue = Object.values(summary)
+ const shortSummary = summary.slice(0, 2)
+ const otherValue = summary
.slice(2)
- .reduce((acc, curr) => acc + curr, 0)
+ .map(({ count }) => count)
+ .reduce((total, item) => total + item, 0)
const updatedSummary = [
- ...shortSummary,
+ ...shortSummary.map(({ name, count }) => [name, count] as const),
...(otherValue > 0
? ([['other', otherValue]] as [string, number][])
: []),
diff --git a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts
index fa674a892f..793f8ec7ac 100644
--- a/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts
+++ b/governance/xc_admin/packages/xc_admin_frontend/components/tabs/Proposals/utils.ts
@@ -1,5 +1,5 @@
import { PythCluster } from '@pythnetwork/client'
-import { AccountMeta } from '@solana/web3.js'
+import { PublicKey } from '@solana/web3.js'
import { MultisigAccount, TransactionAccount } from '@sqds/mesh/lib/types'
import {
ExecutePostedVaa,
@@ -35,26 +35,6 @@ export const getProposalStatus = (
}
}
-/**
- * Sorts the properties of an object by their values in ascending order.
- *
- * @param {Record} obj - The object to sort. All property values should be numbers.
- * @returns {Record} A new object with the same properties as the input, but ordered such that the property with the largest numerical value comes first.
- *
- * @example
- * const obj = { a: 2, b: 3, c: 1 };
- * const sortedObj = sortObjectByValues(obj);
- * console.log(sortedObj); // Outputs: { b: 3, a: 2, c: 1 }
- */
-const sortObjectByValues = (obj: Record) => {
- const sortedEntries = Object.entries(obj).sort(([, a], [, b]) => b - a)
- const sortedObj: Record = {}
- for (const [key, value] of sortedEntries) {
- sortedObj[key] = value
- }
- return sortedObj
-}
-
/**
* Returns a summary of the instructions in a list of multisig instructions.
*
@@ -62,39 +42,78 @@ const sortObjectByValues = (obj: Record) => {
* @param {PythCluster} options.cluster - The Pyth cluster to use for parsing instructions.
* @returns {Record} A summary of the instructions, where the keys are the names of the instructions and the values are the number of times each instruction appears in the list.
*/
-export const getInstructionsSummary = (options: {
+export const getInstructionsSummary = ({
+ instructions,
+ cluster,
+}: {
instructions: MultisigInstruction[]
cluster: PythCluster
-}) => {
- const { instructions, cluster } = options
+}) =>
+ Object.entries(
+ getInstructionSummariesByName(
+ MultisigParser.fromCluster(cluster),
+ instructions
+ )
+ )
+ .map(([name, summaries = []]) => ({
+ name,
+ count: summaries.length ?? 0,
+ summaries,
+ }))
+ .toSorted(({ count }) => count)
- return sortObjectByValues(
- instructions.reduce((acc, instruction) => {
- if (instruction instanceof WormholeMultisigInstruction) {
- const governanceAction = instruction.governanceAction
- if (governanceAction instanceof ExecutePostedVaa) {
- const innerInstructions = governanceAction.instructions
- innerInstructions.forEach((innerInstruction) => {
- const multisigParser = MultisigParser.fromCluster(cluster)
- const parsedInstruction = multisigParser.parseInstruction({
- programId: innerInstruction.programId,
- data: innerInstruction.data as Buffer,
- keys: innerInstruction.keys as AccountMeta[],
- })
- acc[parsedInstruction.name] = (acc[parsedInstruction.name] ?? 0) + 1
- })
- } else if (governanceAction instanceof PythGovernanceActionImpl) {
- acc[governanceAction.action] = (acc[governanceAction.action] ?? 0) + 1
- } else if (governanceAction instanceof SetDataSources) {
- acc[governanceAction.actionName] =
- (acc[governanceAction.actionName] ?? 0) + 1
- } else {
- acc['unknown'] = (acc['unknown'] ?? 0) + 1
- }
- } else {
- acc[instruction.name] = (acc[instruction.name] ?? 0) + 1
- }
- return acc
- }, {} as Record)
+const getInstructionSummariesByName = (
+ parser: MultisigParser,
+ instructions: MultisigInstruction[]
+) =>
+ Object.groupBy(
+ instructions.flatMap((instruction) =>
+ getInstructionSummary(parser, instruction)
+ ),
+ ({ name }) => name
)
+
+const getInstructionSummary = (
+ parser: MultisigParser,
+ instruction: MultisigInstruction
+) => {
+ if (instruction instanceof WormholeMultisigInstruction) {
+ const { governanceAction } = instruction
+ if (governanceAction instanceof ExecutePostedVaa) {
+ return governanceAction.instructions.map((innerInstruction) =>
+ getTransactionSummary(parser.parseInstruction(innerInstruction))
+ )
+ } else if (governanceAction instanceof PythGovernanceActionImpl) {
+ return [{ name: governanceAction.action } as const]
+ } else if (governanceAction instanceof SetDataSources) {
+ return [{ name: governanceAction.actionName } as const]
+ } else {
+ return [{ name: 'unknown' } as const]
+ }
+ } else {
+ return [getTransactionSummary(instruction)]
+ }
+}
+
+const getTransactionSummary = (instruction: MultisigInstruction) => {
+ switch (instruction.name) {
+ case 'addPublisher':
+ return {
+ name: 'addPublisher',
+ priceAccount:
+ instruction.accounts.named['priceAccount'].pubkey.toBase58(),
+ pub: (instruction.args['pub'] as PublicKey).toBase58(),
+ } as const
+ case 'delPublisher':
+ return {
+ name: 'delPublisher',
+ priceAccount:
+ instruction.accounts.named['priceAccount'].pubkey.toBase58(),
+ pub: (instruction.args['pub'] as PublicKey).toBase58(),
+ } as const
+ default:
+ return {
+ name: instruction.name,
+ } as const
+ }
}
diff --git a/governance/xc_admin/packages/xc_admin_frontend/package.json b/governance/xc_admin/packages/xc_admin_frontend/package.json
index 8d722857fb..1b2c51c8e7 100644
--- a/governance/xc_admin/packages/xc_admin_frontend/package.json
+++ b/governance/xc_admin/packages/xc_admin_frontend/package.json
@@ -33,7 +33,6 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.0",
- "typescript": "4.9.4",
"use-debounce": "^9.0.2",
"web3": "^4.8.0",
"@pythnetwork/xc-admin-common": "*"
@@ -47,6 +46,7 @@
"postcss": "^8.4.16",
"prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.1.13",
- "tailwindcss": "^3.1.8"
+ "tailwindcss": "^3.1.8",
+ "typescript": "^5.4.5"
}
}
diff --git a/package-lock.json b/package-lock.json
index 941f1ff360..24c9721989 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5112,7 +5112,6 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.0",
- "typescript": "4.9.4",
"use-debounce": "^9.0.2",
"web3": "^4.8.0"
},
@@ -5125,7 +5124,8 @@
"postcss": "^8.4.16",
"prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.1.13",
- "tailwindcss": "^3.1.8"
+ "tailwindcss": "^3.1.8",
+ "typescript": "^5.4.5"
}
},
"governance/xc_admin/packages/xc_admin_frontend/node_modules/@noble/curves": {
@@ -5691,6 +5691,18 @@
"node": ">=10"
}
},
+ "governance/xc_admin/packages/xc_admin_frontend/node_modules/typescript": {
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"governance/xc_admin/packages/xc_admin_frontend/node_modules/web3": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/web3/-/web3-4.8.0.tgz",
@@ -83138,7 +83150,7 @@
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.0",
"tailwindcss": "^3.1.8",
- "typescript": "4.9.4",
+ "typescript": "^5.4.5",
"use-debounce": "^9.0.2",
"web3": "^4.8.0"
},
@@ -83528,6 +83540,11 @@
"lru-cache": "^6.0.0"
}
},
+ "typescript": {
+ "version": "5.4.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
+ "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="
+ },
"web3": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/web3/-/web3-4.8.0.tgz",