Skip to content
This repository was archived by the owner on Feb 9, 2025. It is now read-only.

Commit 09bd25e

Browse files
what-name0xcen0xcen
authored
Implement transferring out .sol domains from treasury (#1001)
* Start SNS implementation for proposals * Co-authored-by: Chris Nagy <[email protected]> partial * working state no instruction * feat: transfer domains instruction implementation * feat: add name service program to instruction card * feat: decode name service transfer data * style: change casing * move TransferDomainNae * refactor: TransferDomain Instruction * fix: merge errors * fix: next prebuild error in rendering GovAccSelect Co-authored-by: 0xcen <[email protected]> Co-authored-by: Carlos <[email protected]>
1 parent b4e4ff6 commit 09bd25e

File tree

10 files changed

+281
-1
lines changed

10 files changed

+281
-1
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { NAME_PROGRAM_ID } from '@bonfida/spl-name-service'
2+
import { AccountMetaData } from '@solana/spl-governance'
3+
import { Connection, PublicKey } from '@solana/web3.js'
4+
5+
export const NAME_SERVICE_INSTRUCTIONS = {
6+
[NAME_PROGRAM_ID.toBase58()]: {
7+
2: {
8+
name: 'Domain Name Service: Transfer Domain Name',
9+
accounts: [{ name: 'Domain Name Address' }, { name: 'Treasury Account' }],
10+
getDataUI: async (
11+
_connection: Connection,
12+
data: Uint8Array,
13+
_accounts: AccountMetaData[]
14+
) => {
15+
const decodedData = new PublicKey(data.slice(1))
16+
return (
17+
<>
18+
<span>New Owner: {decodedData.toBase58()}</span>
19+
</>
20+
)
21+
},
22+
},
23+
},
24+
}

components/instructions/programs/names.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
LIDO_PROGRAM_ID,
55
LIDO_PROGRAM_ID_DEVNET,
66
} from '@components/TreasuryAccount/ConvertToStSol'
7+
import { NAME_PROGRAM_ID } from '@bonfida/spl-name-service'
78

89
export const GOVERNANCE_PROGRAM_NAMES = {
910
GqTPL6qRf5aUuqscLh8Rg2HTxPUXfhhAXDptTLhp1t2J: 'Mango Governance Program',
@@ -43,6 +44,7 @@ export const PROGRAM_NAMES = {
4344
VotEn9AWwTFtJPJSMV5F9jsMY6QwWM5qn3XP9PATGW7:
4445
'PsyDO Voter Stake Registry Program',
4546
[foresightConsts.PROGRAM_ID]: 'Foresight Dex',
47+
[NAME_PROGRAM_ID.toBase58()]: 'Solana Name Service Program',
4648
AwyKDr1Z5BfdvK3jX1UWopyjsJSV5cq4cuJpoYLofyEn: 'Validator Dao',
4749
Stake11111111111111111111111111111111111111: 'Stake Program',
4850
StakeConfig11111111111111111111111111111111: 'Stake Config',

components/instructions/tools.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ import { PROGRAM_IDS } from '@castlefinance/vault-sdk'
2626
import { FORESIGHT_INSTRUCTIONS } from './programs/foresight'
2727
import { SAGA_PHONE } from './programs/SagaPhone'
2828
import { LIDO_INSTRUCTIONS } from './programs/lido'
29+
import { NAME_SERVICE_INSTRUCTIONS } from './programs/nameService'
2930
import { TOKEN_AUCTION_INSTRUCTIONS } from './programs/tokenAuction'
3031
import { VALIDATORDAO_INSTRUCTIONS } from './programs/validatordao'
32+
3133
/**
3234
* Default governance program id instance
3335
*/
@@ -258,6 +260,7 @@ export const INSTRUCTION_DESCRIPTORS = {
258260
...VOTE_STAKE_REGISTRY_INSTRUCTIONS,
259261
...NFT_VOTER_INSTRUCTIONS,
260262
...STREAMFLOW_INSTRUCTIONS,
263+
...NAME_SERVICE_INSTRUCTIONS,
261264
...SAGA_PHONE,
262265
...TOKEN_AUCTION_INSTRUCTIONS,
263266
...VALIDATORDAO_INSTRUCTIONS,

hooks/useDomainNames.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useEffect, useState } from 'react'
2+
3+
import {
4+
getAllDomains,
5+
performReverseLookupBatch,
6+
} from '@bonfida/spl-name-service'
7+
8+
interface Domains {
9+
domainName: string | undefined
10+
domainAddress: string
11+
}
12+
13+
const useDomainsForAccount = (connection, governedAccount) => {
14+
const [accountDomains, setAccountDomains] = useState<Domains[]>([])
15+
const [isLoading, setIsLoading] = useState(false)
16+
17+
useEffect(() => {
18+
// eslint-disable-next-line @typescript-eslint/no-extra-semi
19+
;(async () => {
20+
setIsLoading(true)
21+
const domains = await getAllDomains(connection, governedAccount.pubkey)
22+
23+
if (domains.length > 0) {
24+
const reverse = await performReverseLookupBatch(connection, domains)
25+
const results: Domains[] = []
26+
27+
for (let i = 0; i < domains.length; i++) {
28+
results.push({
29+
domainAddress: domains[i].toBase58(),
30+
domainName: reverse[i],
31+
})
32+
}
33+
34+
setAccountDomains(results)
35+
}
36+
setIsLoading(false)
37+
})()
38+
}, [governedAccount, connection])
39+
40+
return { accountDomains, isLoading }
41+
}
42+
43+
export { useDomainsForAccount }

hooks/useGovernanceAssets.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { vsrPluginsPks } from './useVotingPlugins'
77

88
export default function useGovernanceAssets() {
99
const { ownVoterWeight, realm, symbol, governances, config } = useRealm()
10+
1011
const governedTokenAccounts: AssetAccount[] = useGovernanceAssetsStore(
1112
(s) => s.governedTokenAccounts
1213
)
@@ -256,6 +257,11 @@ export default function useGovernanceAssets() {
256257
name: 'Withdraw validator stake',
257258
isVisible: canUseAnyInstruction,
258259
},
260+
{
261+
id: Instructions.TransferDomainName,
262+
name: 'SNS Transfer Out Domain Name',
263+
isVisible: canUseAnyInstruction,
264+
},
259265
{
260266
id: Instructions.EverlendDeposit,
261267
name: 'Everlend Deposit Funds',
@@ -503,7 +509,6 @@ export default function useGovernanceAssets() {
503509
},
504510
...foresightInstructions,
505511
]
506-
507512
return {
508513
governancesArray,
509514
getGovernancesByAccountType,

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"dependencies": {
2626
"@blockworks-foundation/mango-client": "^3.6.14",
2727
"@blockworks-foundation/mango-v4": "^0.0.2",
28+
"@bonfida/spl-name-service": "^0.1.47",
2829
"@bundlr-network/client": "^0.7.15",
2930
"@cardinal/namespaces-components": "^2.5.5",
3031
"@castlefinance/vault-core": "^0.1.3",
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import React, { useContext, useEffect, useState } from 'react'
2+
import * as yup from 'yup'
3+
import useWalletStore from 'stores/useWalletStore'
4+
import {
5+
Governance,
6+
ProgramAccount,
7+
serializeInstructionToBase64,
8+
} from '@solana/spl-governance'
9+
import { PublicKey } from '@solana/web3.js'
10+
import { validateInstruction } from '@utils/instructionTools'
11+
import {
12+
DomainNameTransferForm,
13+
UiInstruction,
14+
} from '@utils/uiTypes/proposalCreationTypes'
15+
import { transferInstruction, NAME_PROGRAM_ID } from '@bonfida/spl-name-service'
16+
import { NewProposalContext } from '../../new'
17+
import GovernedAccountSelect from '../GovernedAccountSelect'
18+
19+
import { LoadingDots } from '@components/Loading'
20+
import useGovernanceAssets from '@hooks/useGovernanceAssets'
21+
import Select from '@components/inputs/Select'
22+
import Input from '@components/inputs/Input'
23+
import { useDomainsForAccount } from '@hooks/useDomainNames'
24+
import { isPublicKey } from '@tools/core/pubkey'
25+
26+
const TransferDomainName = ({
27+
index,
28+
governance,
29+
}: {
30+
index: number
31+
governance: ProgramAccount<Governance> | null
32+
}) => {
33+
const connection = useWalletStore((s) => s.connection.current)
34+
const shouldBeGoverned = index !== 0 && governance
35+
const { handleSetInstructions } = useContext(NewProposalContext)
36+
37+
const { assetAccounts } = useGovernanceAssets()
38+
const governedAccount = assetAccounts.filter((acc) => acc.isSol)[0]
39+
const { accountDomains, isLoading } = useDomainsForAccount(
40+
connection,
41+
governedAccount
42+
)
43+
44+
const [formErrors, setFormErrors] = useState({})
45+
const [form, setForm] = useState<DomainNameTransferForm>({
46+
destinationAccount: '',
47+
governedAccount: undefined,
48+
domainAddress: undefined,
49+
})
50+
51+
const handleSetForm = ({ propertyName, value }) => {
52+
setFormErrors({})
53+
setForm({ ...form, [propertyName]: value })
54+
}
55+
56+
async function getInstruction(): Promise<UiInstruction> {
57+
const isValid = await validateInstruction({
58+
schema,
59+
form,
60+
setFormErrors,
61+
})
62+
63+
const obj: UiInstruction = {
64+
serializedInstruction: '',
65+
isValid,
66+
governance: governedAccount?.governance,
67+
}
68+
69+
if (
70+
isValid &&
71+
form.destinationAccount &&
72+
form.domainAddress &&
73+
form.governedAccount
74+
) {
75+
const nameProgramId = new PublicKey(NAME_PROGRAM_ID)
76+
const nameAccountKey = new PublicKey(form.domainAddress)
77+
const newOwnerKey = new PublicKey(form.destinationAccount)
78+
const nameOwner = governedAccount.pubkey
79+
80+
const transferIx = transferInstruction(
81+
nameProgramId,
82+
nameAccountKey,
83+
newOwnerKey,
84+
nameOwner
85+
)
86+
87+
obj.serializedInstruction = serializeInstructionToBase64(transferIx)
88+
}
89+
return obj
90+
}
91+
92+
useEffect(() => {
93+
handleSetInstructions(
94+
{ governedAccount: governedAccount?.governance, getInstruction },
95+
index
96+
)
97+
}, [form])
98+
99+
const schema = yup.object().shape({
100+
governedAccount: yup
101+
.object()
102+
.nullable()
103+
.required('Governed account is required'),
104+
destinationAccount: yup
105+
.string()
106+
.required('Please provide a valid destination account')
107+
.test({
108+
name: 'is-valid-account',
109+
test(val, ctx) {
110+
if (!val || !isPublicKey(val)) {
111+
return ctx.createError({
112+
message: 'Please verify the account address',
113+
})
114+
}
115+
return true
116+
},
117+
}),
118+
domainAddress: yup.string().required('Please select a domain name'),
119+
})
120+
121+
return (
122+
<>
123+
<GovernedAccountSelect
124+
label="Governance"
125+
governedAccounts={assetAccounts.filter((acc) => acc.isSol)}
126+
onChange={(value) => {
127+
handleSetForm({ value, propertyName: 'governedAccount' })
128+
}}
129+
value={governedAccount}
130+
error={formErrors['governedAccount']}
131+
shouldBeGoverned={shouldBeGoverned}
132+
governance={governance}
133+
/>
134+
<Input
135+
label="Destination Account"
136+
value={form.destinationAccount}
137+
type="text"
138+
onChange={(element) =>
139+
handleSetForm({
140+
propertyName: 'destinationAccount',
141+
value: element.target.value,
142+
})
143+
}
144+
error={formErrors['destinationAccount']}
145+
/>
146+
{isLoading ? (
147+
<div className="mt-5">
148+
<div>Looking up accountDomains...</div>
149+
<LoadingDots />
150+
</div>
151+
) : (
152+
<Select
153+
className=""
154+
label="Domain"
155+
value={
156+
form.domainAddress
157+
? accountDomains.find(
158+
(d) => d.domainAddress === form.domainAddress
159+
)?.domainName + '.sol'
160+
: ''
161+
}
162+
placeholder="Please select..."
163+
error={formErrors['domainAddress']}
164+
onChange={(value) => {
165+
handleSetForm({
166+
value: accountDomains.find((d) => d.domainName === value)
167+
?.domainAddress,
168+
propertyName: 'domainAddress',
169+
})
170+
}}
171+
>
172+
{accountDomains?.map(
173+
(domain, index) =>
174+
domain.domainAddress && (
175+
<Select.Option
176+
key={domain.domainName! + index}
177+
value={domain.domainName}
178+
>
179+
<div className="text-fgd-1 mb-2">{domain.domainName}.sol</div>
180+
<div className="">{domain.domainAddress?.toString()}</div>
181+
</Select.Option>
182+
)
183+
)}
184+
</Select>
185+
)}
186+
</>
187+
)
188+
}
189+
190+
export default TransferDomainName

pages/dao/[symbol]/proposal/new.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import PerpEdit from './components/instructions/Mango/MangoV4/PerpEdit'
105105
import Serum3RegisterMarket from './components/instructions/Mango/MangoV4/Serum3RegisterMarket'
106106
import PerpCreate from './components/instructions/Mango/MangoV4/PerpCreate'
107107
import TokenRegisterTrustless from './components/instructions/Mango/MangoV4/TokenRegisterTrustless'
108+
import TransferDomainName from './components/instructions/TransferDomainName'
108109
import DepositForm from './components/instructions/Everlend/DepositForm'
109110
import WithdrawForm from './components/instructions/Everlend/WithdrawForm'
110111
import MakeChangeReferralFeeParams2 from './components/instructions/Mango/MakeChangeReferralFeeParams2'
@@ -713,10 +714,13 @@ const New = () => {
713714
return <CreateTokenMetadata index={idx} governance={governance} />
714715
case Instructions.UpdateTokenMetadata:
715716
return <UpdateTokenMetadata index={idx} governance={governance} />
717+
case Instructions.TransferDomainName:
718+
return <TransferDomainName index={idx} governance={governance}></TransferDomainName>
716719
case Instructions.EverlendDeposit:
717720
return <DepositForm index={idx} governance={governance} />
718721
case Instructions.EverlendWithdraw:
719722
return <WithdrawForm index={idx} governance={governance} />
723+
720724
default:
721725
null
722726
}

utils/uiTypes/proposalCreationTypes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ export interface SplTokenTransferForm {
3232
mintInfo: MintInfo | undefined
3333
}
3434

35+
export interface DomainNameTransferForm {
36+
destinationAccount: string
37+
governedAccount: AssetAccount | undefined
38+
domainAddress: string | undefined
39+
}
40+
3541
export interface CastleDepositForm {
3642
amount: number | undefined
3743
governedTokenAccount: AssetAccount | undefined
@@ -486,6 +492,7 @@ export enum Instructions {
486492
DeactivateValidatorStake,
487493
WithdrawValidatorStake,
488494
DifferValidatorStake,
495+
TransferDomainName,
489496
EverlendDeposit,
490497
EverlendWithdraw,
491498
}

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,7 @@
15661566
"@bonfida/name-offers" "^0.0.1"
15671567
"@ethersproject/sha2" "^5.5.0"
15681568

1569+
15691570
"@bonfida/spl-name-service@^0.1.50":
15701571
version "0.1.50"
15711572
resolved "https://registry.yarnpkg.com/@bonfida/spl-name-service/-/spl-name-service-0.1.50.tgz#462560199f6869fd97c8a19a8a09851ac15191db"

0 commit comments

Comments
 (0)