mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
The explorer shipped three sign-in systems. Two were Blockscout's and both
spoke to its /account/* backend, which this deployment does not run — measured,
every one of those endpoints 404s on api-explore.lux.network:
/account/get_csrf 404 /account/send_otp 404 /account/siwe_message 404
/account/user/info 404 /account/confirm_otp 404 /account/authenticate_via_* 404
So none of it could ever have worked. It is deleted, not re-pointed: the email
+ one-time-code modal, the SIWE wallet login, the CSRF handshake that fired on
every page render, the Dynamic.xyz provider, and the dev page that let you
paste a session cookie by hand. Rewriting any of it against our own backend
would have been building custom auth, which we do not do.
What is left is the standard OIDC client we already had, finished:
* PKCE (S256). This client is public — it runs in a browser and can hold no
secret — so the verifier is the only thing binding a redeemed code to the
page that asked for it. IAM advertises S256 in its discovery document and
we were not sending a challenge at all.
* userinfo went to <issuer>/api/userinfo, which is a 404 and an /api/ path we
do not serve. It is /v1/iam/oauth/userinfo, per IAM's own discovery.
* logout now ends the IdP session (end_session_endpoint) instead of only
dropping our cookie and leaving the user signed in upstream.
All three live in lib/oidc.ts, which already said never to rebuild these paths
at a call site.
The header's provider switch collapses to two states — signed in with IAM, or
account disabled and this is the wallet/settings menu. `rewards` is stated off:
its gate read authProvider === 'auth0', so it was already unreachable.
The ConfigMaps carried NEXT_PUBLIC_OIDC_CLIENT_ID=lux-explore-client-id, a
client that has never existed, so authorize answered 400 unknown client_id.
IAM names clients <org>-explore; all four brands now say so, and hanzo/zoo/pars
get the OIDC keys they never had.
Typecheck: no new error classes, production errors 26 -> 22.
Co-authored-by: Hanzo Dev <[email protected]>
232 lines
8.7 KiB
TypeScript
232 lines
8.7 KiB
TypeScript
import { Button } from '@luxfi/ui/button';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { useRouter } from 'next/router';
|
|
import React from 'react';
|
|
|
|
import type { VerifiedAddress, TokenInfoApplication, TokenInfoApplications, VerifiedAddressResponse } from 'types/api/account';
|
|
|
|
import config from 'configs/app';
|
|
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
|
|
import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType';
|
|
import getQueryParamString from 'lib/router/getQueryParamString';
|
|
import { TOKEN_INFO_APPLICATION, VERIFIED_ADDRESS } from 'stubs/account';
|
|
import { BackToButton } from 'toolkit/components/buttons/BackToButton';
|
|
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
|
import { Link } from 'toolkit/next/link';
|
|
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
|
|
import AccountPageDescription from 'ui/shared/AccountPageDescription';
|
|
import DataListDisplay from 'ui/shared/DataListDisplay';
|
|
import PageTitle from 'ui/shared/Page/PageTitle';
|
|
import AdminSupportText from 'ui/shared/texts/AdminSupportText';
|
|
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
|
|
import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken';
|
|
import TokenInfoForm from 'ui/tokenInfo/TokenInfoForm';
|
|
import VerifiedAddressesListItem from 'ui/verifiedAddresses/VerifiedAddressesListItem';
|
|
import VerifiedAddressesTable from 'ui/verifiedAddresses/VerifiedAddressesTable';
|
|
|
|
const VerifiedAddresses = () => {
|
|
useRedirectForInvalidAuthToken();
|
|
|
|
const router = useRouter();
|
|
const addressHash = getQueryParamString(router.query.address);
|
|
|
|
const [ selectedAddress, setSelectedAddress ] = React.useState<string | undefined>(addressHash);
|
|
|
|
React.useEffect(() => {
|
|
addressHash && router.replace({ pathname: '/account/verified-addresses' });
|
|
// componentDidMount
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [ ]);
|
|
|
|
const modalProps = useDisclosure();
|
|
const queryClient = useQueryClient();
|
|
|
|
const profileQuery = useProfileQuery();
|
|
|
|
const addressesQuery = useApiQuery('contractInfo:verified_addresses', {
|
|
pathParams: { chainId: config.chain.id },
|
|
queryOptions: {
|
|
placeholderData: { verifiedAddresses: Array(3).fill(VERIFIED_ADDRESS) },
|
|
enabled: Boolean(profileQuery.data?.email),
|
|
},
|
|
});
|
|
const applicationsQuery = useApiQuery('admin:token_info_applications', {
|
|
pathParams: { chainId: config.chain.id, id: undefined },
|
|
queryOptions: {
|
|
placeholderData: { submissions: Array(3).fill(TOKEN_INFO_APPLICATION) },
|
|
enabled: Boolean(profileQuery.data?.email),
|
|
select: (data) => {
|
|
return {
|
|
...data,
|
|
submissions: data.submissions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)),
|
|
};
|
|
},
|
|
},
|
|
});
|
|
|
|
const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData;
|
|
const userWithoutEmail = profileQuery.data && !profileQuery.data.email;
|
|
|
|
const handleGoBack = React.useCallback(() => {
|
|
setSelectedAddress(undefined);
|
|
}, []);
|
|
|
|
const handleItemAdd = React.useCallback((address: string) => {
|
|
setSelectedAddress(address);
|
|
}, []);
|
|
const handleItemEdit = React.useCallback((address: string) => {
|
|
setSelectedAddress(address);
|
|
}, []);
|
|
|
|
const handleAddressSubmit = React.useCallback((newItem: VerifiedAddress) => {
|
|
queryClient.setQueryData(
|
|
getResourceKey('contractInfo:verified_addresses', { pathParams: { chainId: config.chain.id } }),
|
|
(prevData: VerifiedAddressResponse | undefined) => {
|
|
if (!prevData) {
|
|
return { verifiedAddresses: [ newItem ] };
|
|
}
|
|
|
|
return {
|
|
verifiedAddresses: [ newItem, ...prevData.verifiedAddresses ],
|
|
};
|
|
});
|
|
}, [ queryClient ]);
|
|
|
|
const handleApplicationSubmit = React.useCallback((newItem: TokenInfoApplication) => {
|
|
setSelectedAddress(undefined);
|
|
queryClient.setQueryData(
|
|
getResourceKey('admin:token_info_applications', { pathParams: { chainId: config.chain.id, id: undefined } }),
|
|
(prevData: TokenInfoApplications | undefined) => {
|
|
if (!prevData) {
|
|
return { submissions: [ newItem ] };
|
|
}
|
|
|
|
const isExisting = prevData.submissions.some((item) => item.id.toLowerCase() === newItem.id.toLowerCase());
|
|
const submissions = isExisting ?
|
|
prevData.submissions.map((item) => item.id.toLowerCase() === newItem.id.toLowerCase() ? newItem : item) :
|
|
[ newItem, ...prevData.submissions ];
|
|
return { submissions };
|
|
});
|
|
}, [ queryClient ]);
|
|
|
|
const addButton = (() => {
|
|
if (userWithoutEmail) {
|
|
return (
|
|
<Button disabled className="mt-8">
|
|
Add address
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Button onClick={ modalProps.onOpen } loadingSkeleton={ isLoading } className="mt-8">
|
|
Add address
|
|
</Button>
|
|
);
|
|
})();
|
|
|
|
if (selectedAddress) {
|
|
const addressInfo = addressesQuery.data?.verifiedAddresses.find(({ contractAddress }) => contractAddress.toLowerCase() === selectedAddress.toLowerCase());
|
|
const tokenName = addressInfo ? `${ addressInfo.metadata.tokenName } (${ addressInfo.metadata.tokenSymbol })` : '';
|
|
const beforeTitle = <BackToButton onClick={ handleGoBack } hint="Back to my verified addresses" className="mr-3"/>;
|
|
|
|
return (
|
|
<>
|
|
<PageTitle title="Token info application form" beforeTitle={ beforeTitle }/>
|
|
<TokenInfoForm
|
|
address={ selectedAddress }
|
|
tokenName={ tokenName }
|
|
application={ applicationsQuery.data?.submissions.find(({ tokenAddress }) => tokenAddress.toLowerCase() === selectedAddress.toLowerCase()) }
|
|
onSubmit={ handleApplicationSubmit }
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const content = (() => {
|
|
if (userWithoutEmail) {
|
|
return null;
|
|
}
|
|
|
|
if (addressesQuery.data?.verifiedAddresses) {
|
|
return (
|
|
<>
|
|
<div className="lg:hidden" key="content-mobile">
|
|
{ addressesQuery.data.verifiedAddresses.map((item, index) => (
|
|
<VerifiedAddressesListItem
|
|
key={ item.contractAddress + (isLoading ? index : '') }
|
|
item={ item }
|
|
application={
|
|
applicationsQuery.data?.submissions
|
|
?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase())
|
|
}
|
|
onAdd={ handleItemAdd }
|
|
onEdit={ handleItemEdit }
|
|
isLoading={ isLoading }
|
|
/>
|
|
)) }
|
|
</div>
|
|
<div className="hidden lg:block" key="content-desktop">
|
|
<VerifiedAddressesTable
|
|
data={ addressesQuery.data.verifiedAddresses }
|
|
applications={ applicationsQuery.data?.submissions }
|
|
onItemEdit={ handleItemEdit }
|
|
onItemAdd={ handleItemAdd }
|
|
isLoading={ isLoading }
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
})();
|
|
|
|
return (
|
|
<>
|
|
<PageTitle title="My verified addresses"/>
|
|
<AccountPageDescription allowCut={ false }>
|
|
<span>
|
|
Verify ownership of a smart contract address to easily update information in the explorer.
|
|
You will sign a single message to verify contract ownership.
|
|
Once verified, you can update token information, address name tags, and address labels from the
|
|
explorer console without needing to sign additional messages.
|
|
</span>
|
|
<p className="font-semibold mt-5">
|
|
Before starting, make sure that:
|
|
</p>
|
|
<ol className="pl-5">
|
|
<li>The source code for the smart contract is deployed on "{ config.chain.name }".</li>
|
|
<li>
|
|
<span>The source code is verified (if not yet verified, you can use </span>
|
|
<Link href="https://docs.lux.network/devs/verification" external noIcon>this tool</Link>
|
|
<span>).</span>
|
|
</li>
|
|
</ol>
|
|
<div className="mt-5">
|
|
Once these steps are complete, click the Add address button below to get started.
|
|
</div>
|
|
<AdminSupportText className="mt-5"/>
|
|
</AccountPageDescription>
|
|
<DataListDisplay
|
|
isError={ profileQuery.isError || addressesQuery.isError || applicationsQuery.isError }
|
|
itemsNum={ addressesQuery.data?.verifiedAddresses.length }
|
|
emptyText=""
|
|
>
|
|
{ content }
|
|
</DataListDisplay>
|
|
{ addButton }
|
|
<AddressVerificationModal
|
|
pageType={ PAGE_TYPE_DICT['/account/verified-addresses'] }
|
|
open={ modalProps.open }
|
|
onOpenChange={ modalProps.onOpenChange }
|
|
onSubmit={ handleAddressSubmit }
|
|
onAddTokenInfoClick={ handleItemAdd }
|
|
onShowListClick={ modalProps.onClose }
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default React.memo(VerifiedAddresses);
|