add domains name support (#336)

This commit is contained in:
DR497 2021-07-13 16:21:39 +08:00 committed by GitHub
parent 81d045e4e6
commit ee796aea2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 415 additions and 2 deletions

View File

@ -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"

View File

@ -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;

View File

@ -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()),
);
};