diff --git a/src/components/BalancesList.js b/src/components/BalancesList.js index 1e17050..9b933a4 100644 --- a/src/components/BalancesList.js +++ b/src/components/BalancesList.js @@ -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() { /> + + setShowDomains(true)}> + + + + ({ + 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(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 ( + setOpen(false)} + > + + + + + Ownership transfer + + + +
+
+ + New Owner + + +
+ setChecked(e.target.checked)} + /> + } + label={ + + I understand that transferring ownership is irreversible + + } + /> + +
+
+
+ ); +}; + +const DomainListItemDetails = ({ domainName }: { domainName: string }) => { + const classes = useStyles(); + const [transferVisible, setTransferVisible] = useState(false); + return ( + <> +
+ + + +
+ + ); +}; + +const DomainListItem = ({ + domainName, + domainKey, +}: { + domainName: string; + domainKey: PublicKey; +}) => { + const classes = useStyles(); + const [open, setOpen] = useState(false); + return ( + <> + setOpen((prev) => !prev)}> + + + + + {open ? : } + + + + + + ); +}; + +const DomainsList = () => { + const [userDomains, userDomainsLoaded] = useUserDomains(); + if (!userDomainsLoaded) { + return ; + } + if (userDomainsLoaded && userDomains?.length === 0) { + return ( + + You don't own any domain + + ); + } + return ( + <> + + {userDomains?.map((d) => { + return ( + + ); + })} + + + ); +}; + +const DomainDialog = ({ + open, + setOpen, +}: { + open: boolean; + setOpen: (arg: boolean) => void; +}) => { + const classes = useStyles(); + return ( + setOpen(false)} className={classes.modal}> + + + + + Your domains + + + + + + + ); +}; + +export default DomainDialog; diff --git a/src/utils/name-service/index.ts b/src/utils/name-service/index.ts index 03657e7..92b48d4 100644 --- a/src/utils/name-service/index.ts +++ b/src/utils/name-service/index.ts @@ -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 { + 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 { + 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()), + ); +};