Skip to content

Commit 8cb720c

Browse files
authored
[xc-admin] add permission/depermission publisher keys for all asset types (#620)
1 parent 0abf0e2 commit 8cb720c

File tree

7 files changed

+310
-11
lines changed

7 files changed

+310
-11
lines changed

governance/xc_admin/packages/xc_admin_frontend/components/ClusterSwitch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const ClusterSwitch = ({ light }: { light?: boolean | null }) => {
5757
]
5858

5959
return (
60-
<Menu as="div" className="relative z-[2] block w-[180px] text-left">
60+
<Menu as="div" className="relative z-[3] block w-[180px] text-left">
6161
{({ open }) => (
6262
<>
6363
<Menu.Button
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { Program } from '@coral-xyz/anchor'
2+
import { Dialog, Menu, Transition } from '@headlessui/react'
3+
import { PythOracle } from '@pythnetwork/client/lib/anchor'
4+
import * as Label from '@radix-ui/react-label'
5+
import { useWallet } from '@solana/wallet-adapter-react'
6+
import { WalletModalButton } from '@solana/wallet-adapter-react-ui'
7+
import { Cluster, PublicKey, TransactionInstruction } from '@solana/web3.js'
8+
import SquadsMesh from '@sqds/mesh'
9+
import { Fragment, useContext, useEffect, useState } from 'react'
10+
import toast from 'react-hot-toast'
11+
import {
12+
getMultisigCluster,
13+
isRemoteCluster,
14+
mapKey,
15+
proposeInstructions,
16+
WORMHOLE_ADDRESS,
17+
} from 'xc_admin_common'
18+
import { ClusterContext } from '../contexts/ClusterContext'
19+
import { usePythContext } from '../contexts/PythContext'
20+
import { PRICE_FEED_MULTISIG } from '../hooks/useMultisig'
21+
import { ProductRawConfig } from '../hooks/usePyth'
22+
import Arrow from '../images/icons/down.inline.svg'
23+
import { capitalizeFirstLetter } from '../utils/capitalizeFirstLetter'
24+
import Spinner from './common/Spinner'
25+
import CloseIcon from './icons/CloseIcon'
26+
27+
const assetTypes = ['All', 'Crypto', 'Equity', 'FX', 'Metal']
28+
29+
const PermissionDepermissionKey = ({
30+
isPermission,
31+
pythProgramClient,
32+
squads,
33+
}: {
34+
isPermission: boolean
35+
pythProgramClient?: Program<PythOracle>
36+
squads?: SquadsMesh
37+
}) => {
38+
const [publisherKey, setPublisherKey] = useState(
39+
'JTmFx5zX9mM94itfk2nQcJnQQDPjcv4UPD7SYj6xDCV'
40+
)
41+
const [selectedAssetType, setSelectedAssetType] = useState('All')
42+
const [isModalOpen, setIsModalOpen] = useState(false)
43+
const [isSubmitButtonLoading, setIsSubmitButtonLoading] = useState(false)
44+
const [priceAccounts, setPriceAccounts] = useState<PublicKey[]>([])
45+
const { cluster } = useContext(ClusterContext)
46+
const { rawConfig, dataIsLoading } = usePythContext()
47+
const { connected } = useWallet()
48+
49+
// get current input value
50+
51+
const handleChange = (event: any) => {
52+
setSelectedAssetType(event.target.value)
53+
setIsModalOpen(true)
54+
}
55+
56+
const closeModal = () => {
57+
setIsModalOpen(false)
58+
}
59+
60+
const onKeyChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
61+
const {
62+
currentTarget: { value },
63+
} = event
64+
setPublisherKey(value)
65+
}
66+
67+
const handleSubmitButton = async () => {
68+
if (pythProgramClient && squads) {
69+
const instructions: TransactionInstruction[] = []
70+
const multisigAuthority = squads.getAuthorityPDA(
71+
PRICE_FEED_MULTISIG[getMultisigCluster(cluster)],
72+
1
73+
)
74+
const isRemote: boolean = isRemoteCluster(cluster)
75+
const multisigCluster: Cluster | 'localnet' = getMultisigCluster(cluster)
76+
const wormholeAddress = WORMHOLE_ADDRESS[multisigCluster]
77+
const fundingAccount = isRemote
78+
? mapKey(multisigAuthority)
79+
: multisigAuthority
80+
priceAccounts.map((priceAccount) => {
81+
isPermission
82+
? pythProgramClient.methods
83+
.addPublisher(new PublicKey(publisherKey))
84+
.accounts({
85+
fundingAccount,
86+
priceAccount: priceAccount,
87+
})
88+
.instruction()
89+
.then((instruction) => instructions.push(instruction))
90+
: pythProgramClient.methods
91+
.delPublisher(new PublicKey(publisherKey))
92+
.accounts({
93+
fundingAccount,
94+
priceAccount: priceAccount,
95+
})
96+
.instruction()
97+
.then((instruction) => instructions.push(instruction))
98+
})
99+
setIsSubmitButtonLoading(true)
100+
try {
101+
const proposalPubkey = await proposeInstructions(
102+
squads,
103+
PRICE_FEED_MULTISIG[getMultisigCluster(cluster)],
104+
instructions,
105+
isRemote,
106+
wormholeAddress
107+
)
108+
toast.success(`Proposal sent! 🚀 Proposal Pubkey: ${proposalPubkey}`)
109+
setIsSubmitButtonLoading(false)
110+
closeModal()
111+
} catch (e: any) {
112+
toast.error(capitalizeFirstLetter(e.message))
113+
setIsSubmitButtonLoading(false)
114+
}
115+
}
116+
}
117+
118+
useEffect(() => {
119+
if (!dataIsLoading) {
120+
const res: PublicKey[] = []
121+
rawConfig.mappingAccounts[0].products.map((product: ProductRawConfig) => {
122+
const publisherExists =
123+
product.priceAccounts[0].publishers.find(
124+
(p) => p.toBase58() === publisherKey
125+
) !== undefined
126+
if (
127+
(selectedAssetType === 'All' ||
128+
product.metadata.asset_type === selectedAssetType) &&
129+
((isPermission &&
130+
product.priceAccounts[0].publishers.length < 32 &&
131+
!publisherExists) ||
132+
(!isPermission && publisherExists))
133+
) {
134+
res.push(product.priceAccounts[0].address)
135+
}
136+
})
137+
setPriceAccounts(res)
138+
}
139+
}, [rawConfig, dataIsLoading, selectedAssetType, isPermission, publisherKey])
140+
141+
return (
142+
<>
143+
<Menu as="div" className="relative z-[2] block w-[200px] text-left">
144+
{({ open }) => (
145+
<>
146+
<Menu.Button
147+
className={`inline-flex w-full items-center justify-between rounded-lg bg-darkGray2 py-3 px-6 text-sm outline-0`}
148+
>
149+
<span className="mr-3">
150+
{isPermission ? 'Permission Key' : 'Depermission Key'}
151+
</span>
152+
<Arrow className={`${open && 'rotate-180'}`} />
153+
</Menu.Button>
154+
<Transition
155+
as={Fragment}
156+
enter="transition ease-out duration-100"
157+
enterFrom="transform opacity-0 scale-95"
158+
enterTo="transform opacity-100 scale-100"
159+
leave="transition ease-in duration-75"
160+
leaveFrom="transform opacity-100 scale-100"
161+
leaveTo="transform opacity-0 scale-95"
162+
>
163+
<Menu.Items className="absolute right-0 mt-2 w-full origin-top-right">
164+
{assetTypes.map((a) => (
165+
<Menu.Item key={a}>
166+
<button
167+
className={`block w-full bg-darkGray py-3 px-6 text-left text-sm hover:bg-darkGray2`}
168+
value={a}
169+
onClick={handleChange}
170+
>
171+
{a}
172+
</button>
173+
</Menu.Item>
174+
))}
175+
</Menu.Items>
176+
</Transition>
177+
</>
178+
)}
179+
</Menu>
180+
<Transition appear show={isModalOpen} as={Fragment}>
181+
<Dialog
182+
as="div"
183+
className="relative z-40"
184+
onClose={() => setIsModalOpen(false)}
185+
>
186+
<Transition.Child
187+
as={Fragment}
188+
enter="ease-out duration-300"
189+
enterFrom="opacity-0"
190+
enterTo="opacity-100"
191+
leave="ease-in duration-200"
192+
leaveFrom="opacity-100"
193+
leaveTo="opacity-0"
194+
>
195+
<div className="fixed inset-0 bg-black bg-opacity-50" />
196+
</Transition.Child>
197+
<div className="fixed inset-0 overflow-y-auto">
198+
<div className="flex min-h-full items-center justify-center p-4 text-center">
199+
<Transition.Child
200+
as={Fragment}
201+
enter="ease-out duration-300"
202+
enterFrom="opacity-0 scale-95"
203+
enterTo="opacity-100 scale-100"
204+
leave="ease-in duration-200"
205+
leaveFrom="opacity-100 scale-100"
206+
leaveTo="opacity-0 scale-95"
207+
>
208+
<Dialog.Panel className="dialogPanel">
209+
<button className="dialogClose" onClick={closeModal}>
210+
<span className="mr-3">close</span> <CloseIcon />
211+
</button>
212+
<div className="max-w-full">
213+
<Dialog.Title as="h3" className="dialogTitle">
214+
{isPermission ? 'Permission' : 'Depermission'} Publisher
215+
Key
216+
</Dialog.Title>
217+
<div className="flex items-center justify-center">
218+
<div className="rounded-full bg-light py-2 px-4 text-sm text-dark">
219+
Asset Type: {selectedAssetType}
220+
</div>
221+
</div>
222+
<div className="mt-6 block items-center justify-center space-y-2 space-x-0 lg:flex lg:space-y-0 lg:space-x-4">
223+
<Label.Root htmlFor="publisherKey">Key</Label.Root>
224+
<input
225+
className="w-full rounded-lg bg-darkGray px-4 py-2 lg:w-3/4"
226+
type="text"
227+
id="publisherKey"
228+
onChange={onKeyChange}
229+
defaultValue={publisherKey}
230+
/>
231+
</div>
232+
<div className="mt-6">
233+
{!connected ? (
234+
<div className="flex justify-center">
235+
<WalletModalButton className="action-btn text-base" />
236+
</div>
237+
) : (
238+
<button
239+
className="action-btn text-base"
240+
onClick={handleSubmitButton}
241+
>
242+
{isSubmitButtonLoading ? (
243+
<Spinner />
244+
) : (
245+
'Submit Proposal'
246+
)}
247+
</button>
248+
)}
249+
</div>
250+
</div>
251+
</Dialog.Panel>
252+
</Transition.Child>
253+
</div>
254+
</div>
255+
</Dialog>
256+
</Transition>
257+
</>
258+
)
259+
}
260+
261+
export default PermissionDepermissionKey

governance/xc_admin/packages/xc_admin_frontend/components/common/Modal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ const Modal: React.FC<{
3737
leaveFrom="opacity-100 scale-100"
3838
leaveTo="opacity-0 scale-95"
3939
>
40-
<Dialog.Panel className="diaglogPanel">
41-
<button className="diaglogClose" onClick={closeModal}>
40+
<Dialog.Panel className="dialogPanel">
41+
<button className="dialogClose" onClick={closeModal}>
4242
<span className="mr-3">close</span> <CloseIcon />
4343
</button>
4444
<div className="max-w-full">
45-
<Dialog.Title as="h3" className="diaglogTitle">
45+
<Dialog.Title as="h3" className="dialogTitle">
4646
Proposed Changes
4747
</Dialog.Title>
4848
{content}

governance/xc_admin/packages/xc_admin_frontend/components/tabs/General.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import ClusterSwitch from '../ClusterSwitch'
2222
import Modal from '../common/Modal'
2323
import Spinner from '../common/Spinner'
2424
import Loadbar from '../loaders/Loadbar'
25+
import PermissionDepermissionKey from '../PermissionDepermissionKey'
2526

2627
const General = () => {
2728
const [data, setData] = useState<any>({})
@@ -689,6 +690,18 @@ const General = () => {
689690
<ClusterSwitch />
690691
</div>
691692
</div>
693+
<div className="relative mt-6 flex space-x-4">
694+
<PermissionDepermissionKey
695+
isPermission={true}
696+
pythProgramClient={pythProgramClient}
697+
squads={squads}
698+
/>
699+
<PermissionDepermissionKey
700+
isPermission={false}
701+
pythProgramClient={pythProgramClient}
702+
squads={squads}
703+
/>
704+
</div>
692705
<div className="relative mt-6">
693706
{dataIsLoading ? (
694707
<div className="mt-3">

governance/xc_admin/packages/xc_admin_frontend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
"@coral-xyz/anchor": "^0.26.0",
1313
"@headlessui/react": "^1.7.7",
1414
"@pythnetwork/client": "^2.15.0",
15-
"@solana/spl-token": "^0.3.7",
15+
"@radix-ui/react-label": "^2.0.0",
1616
"@radix-ui/react-tooltip": "^1.0.3",
17+
"@solana/spl-token": "^0.3.7",
1718
"@solana/wallet-adapter-base": "^0.9.20",
1819
"@solana/wallet-adapter-react": "^0.15.28",
1920
"@solana/wallet-adapter-react-ui": "^0.9.27",

governance/xc_admin/packages/xc_admin_frontend/styles/globals.css

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -269,16 +269,16 @@
269269
@apply hover:bg-pythPurple;
270270
}
271271

272-
.diaglogPanel {
273-
@apply flex h-full min-h-[420px] w-[calc(100%-24px)] max-w-6xl transform items-center justify-center rounded-[40px] bg-[rgba(49,47,71,1)] p-5 px-6 pt-20 pb-8 text-center align-middle shadow-xl transition-all md:mt-[92px] lg:p-10;
272+
.dialogPanel {
273+
@apply flex h-full min-h-[420px] w-[calc(100%-24px)] max-w-6xl transform items-center justify-center rounded-[40px] bg-[rgba(49,47,71,1)] p-5 px-6 pt-20 pb-8 text-center align-middle shadow-xl transition-all md:mt-[92px] lg:p-10;
274274
}
275275

276-
.diaglogClose {
277-
@apply absolute right-10 top-8 flex items-center leading-none;
276+
.dialogClose {
277+
@apply absolute right-10 top-8 flex items-center leading-none;
278278
}
279279

280-
.diaglogTitle {
281-
@apply mb-8 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px];
280+
.dialogTitle {
281+
@apply mb-8 text-center font-body text-[32px] leading-[1.1] lg:mb-11 lg:text-[44px] px-10;
282282
}
283283

284284
.action-btn {

package-lock.json

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)