add domains name support (#336)
This commit is contained in:
parent
81d045e4e6
commit
ee796aea2f
|
@ -57,6 +57,8 @@ import TokenIcon from './TokenIcon';
|
|||
import EditAccountNameDialog from './EditAccountNameDialog';
|
||||
import MergeAccountsDialog from './MergeAccountsDialog';
|
||||
import SwapButton from './SwapButton';
|
||||
import DnsIcon from '@material-ui/icons/Dns';
|
||||
import DomainsList from './DomainsList';
|
||||
|
||||
const balanceFormat = new Intl.NumberFormat(undefined, {
|
||||
minimumFractionDigits: 4,
|
||||
|
@ -108,6 +110,7 @@ export default function BalancesList() {
|
|||
const [showMergeAccounts, setShowMergeAccounts] = useState(false);
|
||||
const [showFtxPayDialog, setShowFtxPayDialog] = useState(false);
|
||||
const [sortAccounts, setSortAccounts] = useState(SortAccounts.None);
|
||||
const [showDomains, setShowDomains] = useState(false);
|
||||
const { accounts, setAccountName } = useWalletSelector();
|
||||
const isExtensionWidth = useIsExtensionWidth();
|
||||
// Dummy var to force rerenders on demand.
|
||||
|
@ -231,6 +234,12 @@ export default function BalancesList() {
|
|||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="See your domains" arrow>
|
||||
<IconButton size={iconSize} onClick={() => setShowDomains(true)}>
|
||||
<DnsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<DomainsList open={showDomains} setOpen={setShowDomains} />
|
||||
<SwapButton size={iconSize} />
|
||||
<Tooltip title="Migrate Tokens" arrow>
|
||||
<IconButton
|
||||
|
@ -607,8 +616,7 @@ function BalanceListItemDetails({
|
|||
<Typography variant="body2">
|
||||
<Link
|
||||
href={
|
||||
`https://solscan.io/account/${publicKey.toBase58()}` +
|
||||
urlSuffix
|
||||
`https://solscan.io/account/${publicKey.toBase58()}` + urlSuffix
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
|
|
|
@ -0,0 +1,323 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
Typography,
|
||||
Paper,
|
||||
ListItemText,
|
||||
ListItem,
|
||||
List,
|
||||
Button,
|
||||
Collapse,
|
||||
ListItemIcon,
|
||||
Toolbar,
|
||||
AppBar,
|
||||
InputLabel,
|
||||
TextField,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
} from '@material-ui/core';
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
import SendIcon from '@material-ui/icons/Send';
|
||||
import { useUserDomains } from '../utils/name-service';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { PublicKey, Transaction } from '@solana/web3.js';
|
||||
import DnsIcon from '@material-ui/icons/Dns';
|
||||
import Modal from '@material-ui/core/Modal';
|
||||
import ExpandLess from '@material-ui/icons/ExpandLess';
|
||||
import ExpandMore from '@material-ui/icons/ExpandMore';
|
||||
import GavelIcon from '@material-ui/icons/Gavel';
|
||||
import { signAndSendTransaction } from '../utils/tokens';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { SOL_TLD_AUTHORITY } from '../utils/name-service';
|
||||
import { transferNameOwnership } from '@bonfida/spl-name-service';
|
||||
import { useConnection } from '../utils/connection';
|
||||
import { useWallet } from '../utils/wallet';
|
||||
import { refreshCache } from '../utils/fetch-loop';
|
||||
import tuple from 'immutable-tuple';
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
address: {
|
||||
textOverflow: 'ellipsis',
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
buttonContainer: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-evenly',
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
itemDetails: {
|
||||
marginLeft: theme.spacing(3),
|
||||
marginRight: theme.spacing(3),
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
text: {
|
||||
color: 'white',
|
||||
fontSize: 24,
|
||||
marginTop: '2%',
|
||||
marginBottom: '2%',
|
||||
opacity: 0.8,
|
||||
},
|
||||
input: {
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
width: 500,
|
||||
},
|
||||
transferContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
paper: {
|
||||
width: '50%',
|
||||
height: '80%',
|
||||
overflowY: 'scroll',
|
||||
},
|
||||
modal: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const TransferDialog = ({
|
||||
domainName,
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
domainName: string;
|
||||
open: boolean;
|
||||
setOpen: (args: boolean) => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const connection = useConnection();
|
||||
const wallet = useWallet();
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [newOwner, setNewOwner] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const onChange = (e) => {
|
||||
setNewOwner(e.target.value.trim());
|
||||
};
|
||||
const canSumbit = newOwner && checked;
|
||||
const onClick = async () => {
|
||||
if (!newOwner) {
|
||||
return enqueueSnackbar('Invalid input', { variant: 'error' });
|
||||
}
|
||||
let newOwnerPubkey: PublicKey;
|
||||
try {
|
||||
newOwnerPubkey = new PublicKey(newOwner);
|
||||
} catch {
|
||||
return enqueueSnackbar('Invalid input', { variant: 'error' });
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
const instructions = await transferNameOwnership(
|
||||
connection,
|
||||
domainName,
|
||||
newOwnerPubkey,
|
||||
undefined,
|
||||
SOL_TLD_AUTHORITY,
|
||||
);
|
||||
await signAndSendTransaction(
|
||||
connection,
|
||||
new Transaction().add(instructions),
|
||||
wallet,
|
||||
[],
|
||||
);
|
||||
enqueueSnackbar('Domain name transfered', { variant: 'success' });
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
console.warn(`Error transferring domain name ${err}`);
|
||||
return enqueueSnackbar('Error transferring domain name', {
|
||||
variant: 'error',
|
||||
});
|
||||
} finally {
|
||||
refreshCache(tuple('useUserDomain', wallet?.publicKey?.toBase58()));
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Modal
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Paper>
|
||||
<AppBar position="sticky" elevation={1}>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" style={{ flexGrow: 1 }} component="h2">
|
||||
Ownership transfer
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<div className={classes.transferContainer}>
|
||||
<div>
|
||||
<InputLabel className={classes.text} shrink>
|
||||
New Owner
|
||||
</InputLabel>
|
||||
<TextField
|
||||
placeholder="New owner"
|
||||
InputProps={{
|
||||
className: classes.input,
|
||||
}}
|
||||
value={newOwner}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={(e) => setChecked(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2">
|
||||
I understand that transferring ownership is irreversible
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={onClick}
|
||||
disabled={!canSumbit || loading}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{ padding: 10 }}
|
||||
>
|
||||
{loading ? <LoadingIndicator height="10px" /> : 'Transfer'}
|
||||
</Button>
|
||||
</div>
|
||||
</Paper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const DomainListItemDetails = ({ domainName }: { domainName: string }) => {
|
||||
const classes = useStyles();
|
||||
const [transferVisible, setTransferVisible] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div className={classes.buttonContainer}>
|
||||
<Button
|
||||
onClick={() => setTransferVisible(true)}
|
||||
startIcon={<SendIcon />}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
>
|
||||
Transfer
|
||||
</Button>
|
||||
<TransferDialog
|
||||
domainName={domainName}
|
||||
open={transferVisible}
|
||||
setOpen={setTransferVisible}
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
window.open(
|
||||
`https://naming.bonfida.org/#/domain-registration/${domainName}`,
|
||||
'_blank',
|
||||
)
|
||||
}
|
||||
startIcon={<GavelIcon />}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
>
|
||||
Sell
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DomainListItem = ({
|
||||
domainName,
|
||||
domainKey,
|
||||
}: {
|
||||
domainName: string;
|
||||
domainKey: PublicKey;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<ListItem onClick={() => setOpen((prev) => !prev)}>
|
||||
<ListItemIcon>
|
||||
<DnsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`${domainName}.sol`}
|
||||
secondary={domainKey.toBase58()}
|
||||
secondaryTypographyProps={{ className: classes.address }}
|
||||
/>
|
||||
{open ? <ExpandLess /> : <ExpandMore />}
|
||||
</ListItem>
|
||||
<Collapse in={open} timeout="auto" unmountOnExit>
|
||||
<DomainListItemDetails domainName={domainName} />
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DomainsList = () => {
|
||||
const [userDomains, userDomainsLoaded] = useUserDomains();
|
||||
if (!userDomainsLoaded) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
if (userDomainsLoaded && userDomains?.length === 0) {
|
||||
return (
|
||||
<Typography variant="body1" align="center">
|
||||
You don't own any domain
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<List>
|
||||
{userDomains?.map((d) => {
|
||||
return (
|
||||
<DomainListItem
|
||||
key={d.nameKey.toBase58()}
|
||||
domainName={d.name}
|
||||
domainKey={d.nameKey}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DomainDialog = ({
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (arg: boolean) => void;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<Modal open={open} onClose={() => setOpen(false)} className={classes.modal}>
|
||||
<Paper className={classes.paper}>
|
||||
<AppBar position="sticky" color="default" elevation={1}>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" style={{ flexGrow: 1 }} component="h2">
|
||||
Your domains
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<DomainsList />
|
||||
</Paper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainDialog;
|
|
@ -4,13 +4,24 @@ import {
|
|||
getHashedName,
|
||||
getNameAccountKey,
|
||||
NameRegistryState,
|
||||
getFilteredProgramAccounts,
|
||||
NAME_PROGRAM_ID,
|
||||
} from '@bonfida/spl-name-service';
|
||||
import { useConnection } from '../connection';
|
||||
import { useWallet } from '../wallet';
|
||||
import BN from 'bn.js';
|
||||
import { useAsyncData } from '../fetch-loop';
|
||||
import tuple from 'immutable-tuple';
|
||||
|
||||
// Address of the SOL TLD
|
||||
export const SOL_TLD_AUTHORITY = new PublicKey(
|
||||
'58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx',
|
||||
);
|
||||
|
||||
export const PROGRAM_ID = new PublicKey(
|
||||
'jCebN34bUfdeUYJT13J1yG16XWQpt5PDx6Mse9GUqhR',
|
||||
);
|
||||
|
||||
export const resolveTwitterHandle = async (
|
||||
connection: Connection,
|
||||
twitterHandle: string,
|
||||
|
@ -45,3 +56,74 @@ export const resolveDomainName = async (
|
|||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export async function findOwnedNameAccountsForUser(
|
||||
connection: Connection,
|
||||
userAccount: PublicKey,
|
||||
): Promise<PublicKey[]> {
|
||||
const filters = [
|
||||
{
|
||||
memcmp: {
|
||||
offset: 32,
|
||||
bytes: userAccount.toBase58(),
|
||||
},
|
||||
},
|
||||
];
|
||||
const accounts = await getFilteredProgramAccounts(
|
||||
connection,
|
||||
NAME_PROGRAM_ID,
|
||||
filters,
|
||||
);
|
||||
return accounts.map((a) => a.publicKey);
|
||||
}
|
||||
|
||||
export async function performReverseLookup(
|
||||
connection: Connection,
|
||||
nameAccount: PublicKey,
|
||||
): Promise<string> {
|
||||
let [centralState] = await PublicKey.findProgramAddress(
|
||||
[PROGRAM_ID.toBuffer()],
|
||||
PROGRAM_ID,
|
||||
);
|
||||
let hashedReverseLookup = await getHashedName(nameAccount.toBase58());
|
||||
let reverseLookupAccount = await getNameAccountKey(
|
||||
hashedReverseLookup,
|
||||
centralState,
|
||||
);
|
||||
|
||||
let name = await NameRegistryState.retrieve(connection, reverseLookupAccount);
|
||||
if (!name.data) {
|
||||
throw new Error('Could not retrieve name data');
|
||||
}
|
||||
let nameLength = new BN(name.data.slice(0, 4), 'le').toNumber();
|
||||
return name.data.slice(4, 4 + nameLength).toString();
|
||||
}
|
||||
|
||||
export const useUserDomains = () => {
|
||||
const wallet = useWallet();
|
||||
const connection = useConnection();
|
||||
const fn = async () => {
|
||||
const domains = await findOwnedNameAccountsForUser(
|
||||
connection,
|
||||
wallet.publicKey,
|
||||
);
|
||||
let names: { name: string; nameKey: PublicKey }[] = [];
|
||||
const fn = async (d) => {
|
||||
try {
|
||||
const name = await performReverseLookup(connection, d);
|
||||
names.push({ name: name, nameKey: d });
|
||||
} catch (err) {
|
||||
console.log(`Passing account ${d.toBase58()} - err ${err}`);
|
||||
}
|
||||
};
|
||||
const promises = domains.map((d) => fn(d));
|
||||
await Promise.allSettled(promises);
|
||||
return names.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
};
|
||||
return useAsyncData(
|
||||
fn,
|
||||
tuple('useUserDomain', wallet?.publicKey?.toBase58()),
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue