Skip to content

Commit 92771b8

Browse files
bhagyamudgal0xShuk
andauthored
feat: add set primary sns domain function for treasury wallet (#138)
* feat: add set primary sns domain function for treasury wallet * fix: yarn.lock * fix: remove redundant code for fetching wallet favorite domain * fix runtime error --------- Co-authored-by: Utkarsh <83659045+0xShuk@users.noreply.github.com>
1 parent 616bf91 commit 92771b8

File tree

10 files changed

+447
-47
lines changed

10 files changed

+447
-47
lines changed

components/DomainCopyButton.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from 'react'
2+
import cx from 'classnames'
3+
import type { PublicKey } from '@solana/web3.js'
4+
import { DocumentDuplicateIcon } from '@heroicons/react/outline'
5+
6+
import Tooltip from '@components/Tooltip'
7+
import { notify } from '@utils/notifications'
8+
9+
interface Props {
10+
domainName: string
11+
domainAddress: PublicKey
12+
className?: string
13+
}
14+
15+
export default function DomainCopyButton(props: Props) {
16+
const base58 = props.domainAddress.toBase58()
17+
const text = `${props.domainName}.sol`
18+
19+
return (
20+
<div className={cx(props.className, 'flex space-x-2 text-white/50')}>
21+
<a
22+
className="cursor-pointer transition-colors hover:text-fgd-1 hover:underline"
23+
href={`https://explorer.solana.com/address/${base58}`}
24+
target="_blank"
25+
rel="noreferrer"
26+
>
27+
{text}
28+
</a>
29+
<button
30+
className="h-[1.25em] w-[1.25em] transition-colors hover:text-fgd-1"
31+
onClick={async () => {
32+
try {
33+
await navigator?.clipboard?.writeText(text)
34+
} catch {
35+
notify({
36+
type: 'error',
37+
message: 'Could not copy domain to clipboard',
38+
})
39+
}
40+
}}
41+
>
42+
<Tooltip content="Copy Domain">
43+
<DocumentDuplicateIcon className="cursor-pointer h-[1.25em] w-[1.25em]" />
44+
</Tooltip>
45+
</button>
46+
</div>
47+
)
48+
}

components/treasuryV2/Details/DomainsDetails/Info/Domain.tsx

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
import React from 'react'
1+
import React, { useState } from 'react'
22
import Link from 'next/link'
33
import cx from 'classnames'
4-
5-
import { ExternalLinkIcon, DotsVerticalIcon } from '@heroicons/react/outline'
4+
import {
5+
ExternalLinkIcon,
6+
DotsVerticalIcon,
7+
StarIcon as StarIconOutline,
8+
} from '@heroicons/react/outline'
9+
import { StarIcon as StarIconSolid } from '@heroicons/react/solid'
610
import { ArrowsHorizontal } from '@carbon/icons-react'
7-
811
import DropdownMenu from '@components/DropdownMenu/DropdownMenu'
912
import Tooltip from '@components/Tooltip'
10-
1113
import useRealm from '@hooks/useRealm'
1214
import useQueryContext from '@hooks/useQueryContext'
13-
1415
import { Domain as DomainModel } from '@models/treasury/Domain'
1516
import useWalletOnePointOh from '@hooks/useWalletOnePointOh'
1617
import { useRealmQuery } from '@hooks/queries/realm'
1718
import { useRealmGovernancesQuery } from '@hooks/queries/governance'
1819
import { useLegacyVoterWeight } from '@hooks/queries/governancePower'
20+
import useCreateProposal from '@hooks/useCreateProposal'
21+
import { useRouter } from 'next/router'
22+
import SetPrimaryDomainModal from './SetPrimaryDomainModal'
1923

2024
interface Props {
2125
domain: DomainModel
@@ -28,12 +32,16 @@ const Domain: React.FC<Props> = (props) => {
2832
const realm = useRealmQuery().data?.result
2933
const governanceItems = useRealmGovernancesQuery().data
3034
const { result: ownVoterWeight } = useLegacyVoterWeight()
35+
const [setPrimaryDomainModalOpen, setSetPrimaryDomainModalOpen] =
36+
useState(false)
3137

38+
const { handleCreateProposal } = useCreateProposal()
3239
const {
3340
symbol,
3441
toManyCommunityOutstandingProposalsForUser,
3542
toManyCouncilOutstandingProposalsForUse,
3643
} = useRealm()
44+
const router = useRouter()
3745

3846
const canCreateProposal =
3947
realm &&
@@ -57,11 +65,54 @@ const Domain: React.FC<Props> = (props) => {
5765
? 'Too many council outstanding proposals. You need to finalize them before creating a new one.'
5866
: ''
5967

68+
const canSetPrimaryDomain = connected && canCreateProposal
69+
const setPrimaryDomainTooltipContent = !canSetPrimaryDomain
70+
? tooltipContent
71+
: props.domain?.isFavorite
72+
? 'Primary domain'
73+
: 'Set as primary domain'
74+
75+
// only sns domains can be set as primary
76+
const isSnsDomain = 'type' in props.domain && props.domain?.type === 'sns'
77+
78+
const governance = governanceItems?.[0]
79+
6080
return (
6181
<div className="flex justify-between items-center px-2 py-6 border-b-[0.5px] border-white/50">
62-
<span className="block text-base font-medium">
63-
{`${props.domain.name}.sol`}
64-
</span>
82+
{governance && setPrimaryDomainModalOpen && (
83+
<SetPrimaryDomainModal
84+
isOpen={setPrimaryDomainModalOpen}
85+
domain={props.domain}
86+
closeModal={() => setSetPrimaryDomainModalOpen(false)}
87+
governance={governance}
88+
/>
89+
)}
90+
<div className="flex items-center gap-2">
91+
<span className="block text-base font-medium">
92+
{`${props.domain.name}.sol`}
93+
</span>
94+
{isSnsDomain && (
95+
<Tooltip content={setPrimaryDomainTooltipContent}>
96+
<button
97+
onClick={() => setSetPrimaryDomainModalOpen(true)}
98+
disabled={props.domain?.isFavorite || !canSetPrimaryDomain}
99+
className={cx(
100+
'text-fgd-2 hover:text-primary-light mt-1.5',
101+
(props.domain?.isFavorite || !canSetPrimaryDomain) &&
102+
'cursor-not-allowed',
103+
!canSetPrimaryDomain && 'opacity-50',
104+
)}
105+
>
106+
{props.domain?.isFavorite ? (
107+
<StarIconSolid className="h-5 w-5 text-primary-light" />
108+
) : (
109+
<StarIconOutline className="h-5 w-5" />
110+
)}
111+
</button>
112+
</Tooltip>
113+
)}
114+
</div>
115+
65116
<div className="flex gap-4 ">
66117
<Link
67118
href={`https://explorer.solana.com/address/${props.domain.address}`}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import Modal from '@components/Modal'
2+
import { Governance, ProgramAccount } from '@solana/spl-governance'
3+
import useCreateProposal from '@hooks/useCreateProposal'
4+
import useQueryContext from '@hooks/useQueryContext'
5+
import { useRouter } from 'next/router'
6+
import useRealm from '@hooks/useRealm'
7+
import React, { useState } from 'react'
8+
import Button from '@components/Button'
9+
import { useVoteByCouncilToggle } from '@hooks/useVoteByCouncilToggle'
10+
import { InstructionDataWithHoldUpTime } from 'actions/createProposal'
11+
import type { UiInstruction } from '@utils/uiTypes/proposalCreationTypes'
12+
import { notify } from '@utils/notifications'
13+
import { setPrimaryDomain } from '@bonfida/spl-name-service'
14+
import { serializeInstructionToBase64 } from '@solana/spl-governance'
15+
import { PublicKey } from '@solana/web3.js'
16+
import { useConnection } from '@solana/wallet-adapter-react'
17+
import type { Domain } from '@models/treasury/Domain'
18+
import VoteBySwitch from 'pages/dao/[symbol]/proposal/components/VoteBySwitch'
19+
import { abbreviateAddress } from '@utils/formatting'
20+
21+
const SetPrimaryDomainModal = ({
22+
closeModal,
23+
isOpen,
24+
governance,
25+
domain,
26+
}: {
27+
closeModal: () => void
28+
isOpen: boolean
29+
governance: ProgramAccount<Governance>
30+
domain: Domain
31+
}) => {
32+
const router = useRouter()
33+
const { symbol } = useRealm()
34+
const { fmtUrlWithCluster } = useQueryContext()
35+
const { handleCreateProposal } = useCreateProposal()
36+
const [creatingProposal, setCreatingProposal] = useState(false)
37+
const { voteByCouncil, shouldShowVoteByCouncilToggle, setVoteByCouncil } =
38+
useVoteByCouncilToggle()
39+
const { connection } = useConnection()
40+
41+
async function handleCreateSetPrimaryDomainProposal() {
42+
setCreatingProposal(true)
43+
try {
44+
const setPrimaryDomainIx = await setPrimaryDomain(
45+
connection,
46+
new PublicKey(domain.address),
47+
new PublicKey(domain.owner),
48+
)
49+
50+
if (setPrimaryDomainIx) {
51+
const instructionData = new InstructionDataWithHoldUpTime({
52+
instruction: {
53+
serializedInstruction:
54+
serializeInstructionToBase64(setPrimaryDomainIx),
55+
isValid: true,
56+
governance,
57+
},
58+
governance,
59+
})
60+
61+
const proposalAddress = await handleCreateProposal({
62+
title: `Set ${
63+
domain.name
64+
}.sol as primary domain for ${abbreviateAddress(domain.owner)}`,
65+
description: `This proposal will set ${
66+
domain.name
67+
}.sol as the primary domain for the
68+
wallet ${abbreviateAddress(domain.owner)}.`,
69+
voteByCouncil,
70+
instructionsData: [instructionData],
71+
governance,
72+
})
73+
74+
const url = fmtUrlWithCluster(
75+
`/dao/${symbol}/proposal/${proposalAddress}`,
76+
)
77+
78+
router.push(url)
79+
}
80+
} catch (error) {
81+
notify({ type: 'error', message: `${error}` })
82+
console.error('Failed to create set primary domain proposal', error)
83+
} finally {
84+
setCreatingProposal(false)
85+
}
86+
}
87+
88+
return (
89+
<Modal sizeClassName="sm:max-w-3xl" onClose={closeModal} isOpen={isOpen}>
90+
<div className="w-full space-y-4">
91+
<h3 className="flex flex-col mb-4">Set Primary Domain</h3>
92+
93+
<p>
94+
This proposal will set {domain.name}.sol as the primary domain for the
95+
wallet {abbreviateAddress(domain.owner)}.
96+
</p>
97+
98+
{shouldShowVoteByCouncilToggle && (
99+
<VoteBySwitch
100+
checked={voteByCouncil}
101+
onChange={() => {
102+
setVoteByCouncil(!voteByCouncil)
103+
}}
104+
></VoteBySwitch>
105+
)}
106+
</div>
107+
<div className="flex justify-end pt-6 mt-6 space-x-4 border-t border-fgd-4">
108+
<Button
109+
isLoading={creatingProposal}
110+
disabled={creatingProposal}
111+
onClick={handleCreateSetPrimaryDomainProposal}
112+
>
113+
Add proposal
114+
</Button>
115+
</div>
116+
</Modal>
117+
)
118+
}
119+
120+
export default SetPrimaryDomainModal

components/treasuryV2/Details/WalletDetails/Header.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import AddAssetModal from './AddAssetModal'
2121
import SelectedWalletIcon from '../../icons/SelectedWalletIcon'
2222
import { AssetAccount, AccountType } from '@utils/uiTypes/assets'
2323
import useLegacyConnectionContext from '@hooks/useLegacyConnectionContext'
24+
import DomainCopyButton from '@components/DomainCopyButton'
2425

2526
enum ModalType {
2627
AddAsset,
@@ -69,6 +70,13 @@ export default function Header(props: Props) {
6970
</div>
7071
</div>
7172
<Address address={props.wallet.address} className="ml-14 text-xs" />
73+
{props.wallet?.favoriteDomain && (
74+
<DomainCopyButton
75+
className="ml-14 text-xs"
76+
domainName={props.wallet.favoriteDomain.name}
77+
domainAddress={props.wallet.favoriteDomain.address}
78+
/>
79+
)}
7280
</div>
7381
<div className="flex flex-col space-y-2">
7482
<SecondaryButton

components/treasuryV2/WalletList/WalletListItem/SummaryButton.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ export default function SummaryButton(props: Props) {
110110
/>
111111
</div>
112112
<div className="font-bold text-left whitespace-nowrap text-ellipsis overflow-hidden">
113-
{props.wallet.name || abbreviateAddress(props.wallet.address)}
113+
{props.wallet.name ||
114+
(props.wallet.favoriteDomain?.name
115+
? `${props.wallet.favoriteDomain?.name}.sol`
116+
: abbreviateAddress(props.wallet.address))}
114117
</div>
115118
</div>
116119
</div>

hooks/useTreasuryInfo/assembleWallets.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
toUiDecimals,
4242
} from '@blockworks-foundation/mango-v4'
4343
import { BN } from '@coral-xyz/anchor'
44+
import { getFavoriteDomain } from '@bonfida/spl-name-service'
4445

4546
function isNotNull<T>(x: T | null): x is T {
4647
return x !== null
@@ -145,6 +146,25 @@ export const assembleWallets = async (
145146
const governanceAddress = account.governance?.pubkey?.toBase58()
146147

147148
if (!walletMap[walletAddress]) {
149+
// Fetch favorite domain when creating a new wallet
150+
let favoriteDomain: {
151+
name: string
152+
address: PublicKey
153+
} | null = null
154+
155+
try {
156+
const favoriteDomainResponse = await getFavoriteDomain(
157+
connection.current,
158+
new PublicKey(walletAddress),
159+
)
160+
favoriteDomain = {
161+
name: favoriteDomainResponse?.reverse,
162+
address: new PublicKey(favoriteDomainResponse?.domain),
163+
}
164+
} catch (error) {
165+
console.error('Error fetching favorite domain', error)
166+
}
167+
148168
walletMap[walletAddress] = {
149169
governanceAddress,
150170
address: walletAddress,
@@ -153,6 +173,7 @@ export const assembleWallets = async (
153173
rules: {},
154174
stats: {},
155175
totalValue: new BigNumber(0),
176+
favoriteDomain,
156177
}
157178

158179
if (governanceAddress) {
@@ -245,11 +266,17 @@ export const assembleWallets = async (
245266
}
246267
}
247268

269+
// Add isFavorite property to each domain using the already fetched favoriteDomain to figure out if the domain is the favorite domain
270+
const domainsWithFavorite = domainList.map((domain) => ({
271+
...domain,
272+
isFavorite: domain.name === walletMap[walletAddress].favoriteDomain?.name,
273+
}))
274+
248275
walletMap[walletAddress].assets.unshift({
249276
type: AssetType.Domain,
250277
id: 'domain-list',
251278
count: new BigNumber(domainList.length),
252-
list: domainList,
279+
list: domainsWithFavorite,
253280
})
254281
}
255282

models/treasury/Domain.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export interface Domain {
22
owner: string
33
name?: string
44
address: string
5+
isFavorite?: boolean
56
}

models/treasury/Wallet.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
} from '@solana/spl-governance'
77

88
import { Asset, Mango, Sol, Token } from './Asset'
9+
import type { PublicKey } from '@metaplex-foundation/js'
910

1011
interface CommonRules {
1112
maxVotingTime: number
@@ -37,6 +38,10 @@ export interface Wallet {
3738
votingProposalCount?: number
3839
}
3940
totalValue: BigNumber
41+
favoriteDomain?: {
42+
name: string
43+
address: PublicKey
44+
} | null
4045
}
4146

4247
export interface AuxiliaryWallet {

0 commit comments

Comments
 (0)